From da24ae70b61aea557b31abc90e322ddf05f2818f Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sat, 3 Dec 2022 11:52:32 +0100 Subject: [PATCH] Implement CA certificate information handling Change the behaviour of the client to use the new CAInfoCommand support in cacert-gosigner. The client has a new mechanism to generate new commands as reaction to received responses. This functionality is used to retrieve CA certificate information when certificates previously unknown to the client appear and to trigger CRL retrieval for new certificates. New CA certificates announced by the signer are detected and information is retrieved. The retrieved CA certificate is stored alongside the CRL files in a configurable directory (defaults to "public" in the working directory of the signerclient process). --- .gitignore | 2 +- go.mod | 2 +- go.sum | 6 + internal/client/client.go | 326 ++++++++++++++++++++++++++---------- internal/command/command.go | 55 ++++++ internal/config/config.go | 14 +- internal/handler/handler.go | 51 +++--- 7 files changed, 337 insertions(+), 119 deletions(-) create mode 100644 internal/command/command.go diff --git a/.gitignore b/.gitignore index 514365b..95d3ad9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ *Pty /.idea/ /config.yaml -/crls/ /dist/ +/public/ /signerclient go.work go.work.sum diff --git a/go.mod b/go.mod index c483863..052ca2c 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.cacert.org/cacert-gosignerclient go 1.19 require ( - git.cacert.org/cacert-gosigner v0.0.0-20221202080952-37d3b1e02146 + git.cacert.org/cacert-gosigner v0.0.0-20221203104439-bc81ab84cb4a github.com/balacode/go-delta v0.1.0 github.com/shamaton/msgpackgen v0.3.0 github.com/sirupsen/logrus v1.9.0 diff --git a/go.sum b/go.sum index 313ae17..6eabf0b 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,11 @@ git.cacert.org/cacert-gosigner v0.0.0-20221202080952-37d3b1e02146 h1:vbm3fIRxNKD4jahqVnIvvU7jc57JfHz5KijalJFlHJ4= git.cacert.org/cacert-gosigner v0.0.0-20221202080952-37d3b1e02146/go.mod h1:OGIB5wLUhJiBhTzSXReOhGxuy7sT5VvyOyT8Ux8EGyw= +git.cacert.org/cacert-gosigner v0.0.0-20221202122810-6f8ac9818cd1 h1:HRtgcV6tRM+jN8NxPx7DkuwdH2prZOTdydMBCFg/CWM= +git.cacert.org/cacert-gosigner v0.0.0-20221202122810-6f8ac9818cd1/go.mod h1:OGIB5wLUhJiBhTzSXReOhGxuy7sT5VvyOyT8Ux8EGyw= +git.cacert.org/cacert-gosigner v0.0.0-20221202173159-afe7d23c9b6f h1:VcIwyogvdmYDpDwE7U0+S2P+xU5zwquppAVp2q4eI9k= +git.cacert.org/cacert-gosigner v0.0.0-20221202173159-afe7d23c9b6f/go.mod h1:OGIB5wLUhJiBhTzSXReOhGxuy7sT5VvyOyT8Ux8EGyw= +git.cacert.org/cacert-gosigner v0.0.0-20221203104439-bc81ab84cb4a h1:yX3lhEoBQkUKu23xggAzAeYWuziCkRYktSjsAOfNGHY= +git.cacert.org/cacert-gosigner v0.0.0-20221203104439-bc81ab84cb4a/go.mod h1:OGIB5wLUhJiBhTzSXReOhGxuy7sT5VvyOyT8Ux8EGyw= 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/go.mod h1:pLeSAL3DhZ9L0JuiRkUtIX3mLOCtzBLnDhfmykbSmkE= diff --git a/internal/client/client.go b/internal/client/client.go index d841bfd..3a2aa50 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -20,6 +20,7 @@ package client import ( "context" "crypto/x509" + "encoding/pem" "fmt" "math/big" "os" @@ -33,6 +34,7 @@ import ( "git.cacert.org/cacert-gosigner/pkg/messages" "git.cacert.org/cacert-gosigner/pkg/protocol" + "git.cacert.org/cacert-gosignerclient/internal/command" "git.cacert.org/cacert-gosignerclient/internal/config" ) @@ -49,30 +51,43 @@ type Profile struct { } type CertInfo struct { - Name string - FetchCRL bool + Name string + FetchCert bool + FetchCRL bool + LastKnownCRL *big.Int + Certificate *x509.Certificate + Profiles map[string]*Profile } type SignerInfo struct { SignerHealth bool SignerVersion string - CACertificates []CertInfo - UsableProfiles map[string][]Profile + CACertificates []string +} + +func (i *SignerInfo) containsCA(caName string) bool { + for _, name := range i.CACertificates { + if name == caName { + return true + } + } + + return false } type Client struct { - port *serial.Port - logger *logrus.Logger - framer protocol.Framer - in chan []byte - out chan []byte - commands chan *protocol.Command - handler protocol.ClientHandler - config *config.ClientConfig - signerInfo *SignerInfo - callback chan interface{} + port *serial.Port + logger *logrus.Logger + framer protocol.Framer + in chan []byte + out chan []byte + commands chan *protocol.Command + handler protocol.ClientHandler + config *config.ClientConfig + signerInfo *SignerInfo + knownCertificates map[string]*CertInfo + callback chan interface{} sync.Mutex - lastKnownCRLS map[string]*big.Int } func (c *Client) Run(ctx context.Context) error { @@ -158,63 +173,118 @@ func (c *Client) commandLoop(ctx context.Context) { fetchCRLTimer := time.NewTimer(c.config.FetchCRLStart) for { + newCommands := make([]*protocol.Command, 0) + select { case <-ctx.Done(): return case callbackData := <-c.callback: - err := c.handleCallback(callbackData) + addCommands, err := c.handleCallback(callbackData) if err != nil { c.logger.WithError(err).Error("callback handling failed") } + + newCommands = append(newCommands, addCommands...) case <-fetchCRLTimer.C: - for _, crlInfo := range c.buildCRLInfo() { - var lastKnown []byte - - if crlInfo.LastKnown != nil { - lastKnown = crlInfo.LastKnown.Bytes() - } - - c.commands <- &protocol.Command{ - Announce: messages.BuildCommandAnnounce(messages.CmdFetchCRL), - Command: &messages.FetchCRLCommand{ - IssuerID: crlInfo.Name, - LastKnownID: lastKnown, - }, - } + for _, crlInfo := range c.requiredCRLs() { + newCommands = append(newCommands, command.FetchCRL(crlInfo.Name, crlInfo.LastKnown)) } fetchCRLTimer.Reset(c.config.FetchCRLInterval) case <-healthTimer.C: - c.commands <- &protocol.Command{ - Announce: messages.BuildCommandAnnounce(messages.CmdHealth), - Command: &messages.HealthCommand{}, - } + newCommands = append(newCommands, command.Health()) healthTimer.Reset(c.config.HealthInterval) } + + for _, nextCommand := range newCommands { + select { + case <-ctx.Done(): + return + case c.commands <- nextCommand: + c.logger.WithField("command", nextCommand.Announce).Trace("sent command") + } + } } } -func (c *Client) handleCallback(data interface{}) error { +func (c *Client) handleCallback(data interface{}) ([]*protocol.Command, error) { switch d := data.(type) { case SignerInfo: - c.updateSignerInfo(d) + return c.updateSignerInfo(d) + case *messages.CAInfoResponse: + return c.updateCAInformation(d) case *messages.FetchCRLResponse: - c.updateCRL(d) + return c.updateCRL(d) default: - return fmt.Errorf("unknown callback data of type %T", data) + return nil, fmt.Errorf("unknown callback data of type %T", data) } +} - return nil +func (c *Client) updateSignerInfo(signerInfo SignerInfo) ([]*protocol.Command, error) { + c.logger.Debug("update signer info") + + c.Lock() + c.signerInfo = &signerInfo + c.Unlock() + + c.learnNewCACertificates() + + c.forgetRemovedCACertificates() + + newCommands := make([]*protocol.Command, 0) + + for _, caName := range c.requiredCertificateInfo() { + newCommands = append(newCommands, command.CAInfo(caName)) + } + + return newCommands, nil } -func (c *Client) updateSignerInfo(signerInfo SignerInfo) { +func (c *Client) updateCAInformation(d *messages.CAInfoResponse) ([]*protocol.Command, error) { c.Lock() defer c.Unlock() - c.logger.Debug("update signer info") + caInfo, ok := c.knownCertificates[d.Name] + if !ok { + c.logger.WithField("certificate", d.Name).Warn("unknown CA certificate") - c.signerInfo = &signerInfo + return nil, nil + } + + cert, err := x509.ParseCertificate(d.Certificate) + if err != nil { + return nil, fmt.Errorf("could not parse CA certificate for %s: %w", d.Name, err) + } + + if !cert.IsCA { + return nil, fmt.Errorf("certificate for %s is not a CA certificate", d.Name) + } + + err = c.writeCertificate(caInfo.Name, d.Certificate) + if err != nil { + c.logger.WithError(err).WithField("certificate", d.Name).Warn("could not write CA certificate files") + } + + caInfo.Certificate = cert + caInfo.FetchCert = false + + caInfo.Profiles = make(map[string]*Profile) + + for _, p := range d.Profiles { + caInfo.Profiles[p.Name] = &Profile{ + Name: p.Name, + UseFor: p.UseFor.String(), + } + } + + if len(cert.CRLDistributionPoints) == 0 { + caInfo.FetchCRL = false + + return nil, nil + } + + return []*protocol.Command{command.FetchCRL(caInfo.Name, c.lastKnownCRL(caInfo))}, nil } type CRLInfo struct { @@ -222,43 +292,63 @@ type CRLInfo struct { LastKnown *big.Int } -func (c *Client) buildCRLInfo() []CRLInfo { +func (c *Client) requiredCRLs() []CRLInfo { c.Lock() defer c.Unlock() - if c.signerInfo == nil { - c.logger.Warn("no signer info available") + if c.knownCertificates == nil { + c.logger.Warn("no certificates known") return nil } infos := make([]CRLInfo, 0) - for _, caInfo := range c.signerInfo.CACertificates { + for _, caInfo := range c.knownCertificates { if caInfo.FetchCRL { - lastKnown := c.lastKnownCRL(caInfo.Name) + infos = append(infos, CRLInfo{Name: caInfo.Name, LastKnown: c.lastKnownCRL(caInfo)}) + } + } + + return infos +} + +func (c *Client) requiredCertificateInfo() []string { + c.Lock() + defer c.Unlock() + + if c.knownCertificates == nil { + c.logger.Warn("no certificates known") + + return nil + } + + infos := make([]string, 0) - infos = append(infos, CRLInfo{Name: caInfo.Name, LastKnown: lastKnown}) + for _, caInfo := range c.knownCertificates { + if caInfo.FetchCert { + infos = append(infos, caInfo.Name) } } return infos } -func (c *Client) lastKnownCRL(caName string) *big.Int { +func (c *Client) lastKnownCRL(caInfo *CertInfo) *big.Int { + caName := caInfo.Name + 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 { + lastKnown := caInfo.LastKnownCRL + + if lastKnown == nil { derData, err := os.ReadFile(crlFileName) if err != nil { c.logger.WithError(err).WithField("crl", crlFileName).Error("could not read CRL data") @@ -274,27 +364,27 @@ func (c *Client) lastKnownCRL(caName string) *big.Int { } 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) { +func (c *Client) updateCRL(d *messages.FetchCRLResponse) ([]*protocol.Command, error) { var ( - der []byte crlNumber *big.Int + der []byte + err error ) + caInfo, ok := c.knownCertificates[d.IssuerID] + if !ok { + c.logger.WithField("certificate", d.IssuerID).Warn("unknown CA certificate") + } + if d.UnChanged { c.logger.WithField("issuer", d.IssuerID).Debug("CRL did not change") - return + return nil, nil } if !d.IsDelta { @@ -304,29 +394,25 @@ func (c *Client) updateCRL(d *messages.FetchCRLResponse) { if err != nil { c.logger.WithError(err).Error("CRL from signer could not be parsed") - return + return nil, nil } crlNumber = list.Number } else { crlFileName := c.buildCRLFileName(d.IssuerID) - der, err := c.patchCRL(crlFileName, d.CRLData) + der, err = c.patchCRL(crlFileName, d.CRLData) if err != nil { c.logger.WithError(err).Error("CRL patching failed") - delete(c.lastKnownCRLS, d.IssuerID) - - return + return nil, nil } list, err := x509.ParseRevocationList(der) if err != nil { c.logger.WithError(err).Error("could not parse patched CRL") - delete(c.lastKnownCRLS, d.IssuerID) - - return + return nil, nil } crlNumber = list.Number @@ -335,17 +421,57 @@ func (c *Client) updateCRL(d *messages.FetchCRLResponse) { if err := c.writeCRL(d.IssuerID, der); err != nil { c.logger.WithError(err).Error("could not store CRL") - delete(c.lastKnownCRLS, d.IssuerID) + caInfo.LastKnownCRL = nil + + return nil, nil + } + + caInfo.LastKnownCRL = crlNumber + + return nil, nil +} + +func (c *Client) buildCRLFileName(caName string) string { + return path.Join(c.config.PublicDataDirectory, fmt.Sprintf("%s.crl", caName)) +} - return +func (c *Client) buildCertificateFileName(caName string, certFormat string) string { + return path.Join(c.config.PublicDataDirectory, fmt.Sprintf("%s.%s", caName, certFormat)) +} + +func (c *Client) ensurePublicDataDirectory() error { + if err := os.MkdirAll(c.config.PublicDataDirectory, worldReadableDirPerm); err != nil { + return fmt.Errorf("could not create public CA data directory %s: %w", c.config.PublicDataDirectory, err) } - c.lastKnownCRLS[d.IssuerID] = crlNumber + return nil +} + +func (c *Client) writeCertificate(caName string, derBytes []byte) error { + if err := c.ensurePublicDataDirectory(); err != nil { + return err + } + + if err := os.WriteFile( + c.buildCertificateFileName(caName, "crt"), derBytes, worldReadableFilePerm, + ); err != nil { + c.logger.WithError(err).Error("could not write DER encoded certificate file") + } + + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + + if err := os.WriteFile( + c.buildCertificateFileName(caName, "pem"), pemBytes, worldReadableFilePerm, + ); err != nil { + c.logger.WithError(err).Error("could not write PEM encoded certificate file") + } + + return nil } 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 := c.ensurePublicDataDirectory(); err != nil { + return err } if err := os.WriteFile(c.buildCRLFileName(caName), crlBytes, worldReadableFilePerm); err != nil { @@ -374,6 +500,38 @@ func (c *Client) patchCRL(crlFileName string, diff []byte) ([]byte, error) { return der, nil } +func (c *Client) learnNewCACertificates() { + c.Lock() + defer c.Unlock() + + for _, caName := range c.signerInfo.CACertificates { + if _, ok := c.knownCertificates[caName]; ok { + continue + } + + c.knownCertificates[caName] = &CertInfo{ + Name: caName, + FetchCert: true, + FetchCRL: true, + } + } +} + +func (c *Client) forgetRemovedCACertificates() { + c.Lock() + defer c.Unlock() + + for knownCA := range c.knownCertificates { + if c.signerInfo.containsCA(knownCA) { + continue + } + + c.logger.WithField("certificate", knownCA).Warn("signer did not send status for certificate") + + delete(c.knownCertificates, knownCA) + } +} + func New( cfg *config.ClientConfig, logger *logrus.Logger, @@ -387,15 +545,15 @@ func New( } client := &Client{ - logger: logger, - framer: cobsFramer, - in: make(chan []byte), - out: make(chan []byte), - commands: commands, - handler: handler, - config: cfg, - callback: callback, - lastKnownCRLS: make(map[string]*big.Int), + logger: logger, + framer: cobsFramer, + in: make(chan []byte), + out: make(chan []byte), + commands: commands, + handler: handler, + config: cfg, + callback: callback, + knownCertificates: make(map[string]*CertInfo), } err = client.setupConnection(&serial.Config{ diff --git a/internal/command/command.go b/internal/command/command.go new file mode 100644 index 0000000..60f51a7 --- /dev/null +++ b/internal/command/command.go @@ -0,0 +1,55 @@ +/* +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 command + +import ( + "math/big" + + "git.cacert.org/cacert-gosigner/pkg/messages" + "git.cacert.org/cacert-gosigner/pkg/protocol" +) + +func CAInfo(caName string) *protocol.Command { + return &protocol.Command{ + Announce: messages.BuildCommandAnnounce(messages.CmdCAInfo), + Command: &messages.CAInfoCommand{Name: caName}, + } +} + +func FetchCRL(caName string, crlNumber *big.Int) *protocol.Command { + var lastKnown []byte + + if crlNumber != nil { + lastKnown = crlNumber.Bytes() + } + + return &protocol.Command{ + Announce: messages.BuildCommandAnnounce(messages.CmdFetchCRL), + Command: &messages.FetchCRLCommand{ + IssuerID: caName, + LastKnownID: lastKnown, + }, + } +} + +func Health() *protocol.Command { + return &protocol.Command{ + Announce: messages.BuildCommandAnnounce(messages.CmdHealth), + Command: &messages.HealthCommand{}, + } +} diff --git a/internal/config/config.go b/internal/config/config.go index dd69f10..44eccfd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -31,10 +31,10 @@ const ( defaultHealthInterval = 30 * time.Second defaultHealthStart = 2 * time.Second defaultFetchCRLInterval = 5 * time.Minute - defaultFetchCRLStart = 5 * time.Second + defaultFetchCRLStart = 5 * time.Minute defaultResponseAnnounceTimeout = 30 * time.Second defaultResponseDataTimeout = 2 * time.Second - defaultCRLDirectory = "crls" + defaultFilesDirectory = "public" ) type SettingsError struct { @@ -59,7 +59,7 @@ type ClientConfig struct { 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"` + PublicDataDirectory string `yaml:"public-data-directory"` } func (c *ClientConfig) UnmarshalYAML(n *yaml.Node) error { @@ -71,7 +71,7 @@ func (c *ClientConfig) UnmarshalYAML(n *yaml.Node) error { 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"` + PublicDataDirectory string `yaml:"public-data-directory"` }{} err := n.Decode(&data) @@ -129,11 +129,11 @@ func (c *ClientConfig) UnmarshalYAML(n *yaml.Node) error { c.ResponseDataTimeout = data.ResponseDataTimeout - if data.CRLDirectory == "" { - data.CRLDirectory = defaultCRLDirectory + if data.PublicDataDirectory == "" { + data.PublicDataDirectory = defaultFilesDirectory } - c.CRLDirectory = data.CRLDirectory + c.PublicDataDirectory = data.PublicDataDirectory return nil } diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 937359c..cc93efb 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -112,6 +112,13 @@ func (s *SignerClientHandler) ResponseData(ctx context.Context, in chan []byte, return fmt.Errorf("could not unmarshal health response data: %w", err) } + response.Response = &resp + case messages.RespCAInfo: + var resp messages.CAInfoResponse + if err := msgpack.Unmarshal(frame, &resp); err != nil { + return fmt.Errorf("could not unmarshal CA info response data: %w", err) + } + response.Response = &resp case messages.RespFetchCRL: var resp messages.FetchCRLResponse @@ -146,6 +153,8 @@ func (s *SignerClientHandler) HandleResponse(ctx context.Context, response *prot s.logger.WithField("message", r.Message).Error("error from signer") case *messages.HealthResponse: s.handleHealthResponse(ctx, r) + case *messages.CAInfoResponse: + s.handleCAInfoResponse(ctx, r) case *messages.FetchCRLResponse: s.handleFetchCRLResponse(ctx, r) default: @@ -173,38 +182,19 @@ func (s *SignerClientHandler) handleHealthResponse(ctx context.Context, r *messa switch item.Source { case "HSM": - signerInfo.CACertificates = make([]client.CertInfo, 0) - signerInfo.UsableProfiles = make(map[string][]client.Profile) + signerInfo.CACertificates = make([]string, 0) - for certName, value := range item.MoreInfo { - certInfo, err := messages.ParseCertificateInfo(value) - if err != nil { - s.logger.WithError(err).Error("could not parse certificate information") + for certName, status := range item.MoreInfo { + if status == string(messages.CertStatusOk) { + signerInfo.CACertificates = append(signerInfo.CACertificates, certName) - break + continue } 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, - client.CertInfo{Name: certName, FetchCRL: certInfo.Signing}, - ) - - if certInfo.Signing { - for _, profile := range certInfo.Profiles { - signerInfo.UsableProfiles[certName] = append( - signerInfo.UsableProfiles[certName], - client.Profile{Name: profile.Name, UseFor: string(profile.UseFor)}, - ) - } - } + "status": status, + }).Warn("certificate has issues") } default: s.logger.WithField("source", item.Source).Warn("unhandled health source") @@ -219,6 +209,15 @@ func (s *SignerClientHandler) handleHealthResponse(ctx context.Context, r *messa } } +func (s *SignerClientHandler) handleCAInfoResponse(ctx context.Context, r *messages.CAInfoResponse) { + select { + case <-ctx.Done(): + return + case s.clientCallback <- r: + break + } +} + func (s *SignerClientHandler) handleFetchCRLResponse(ctx context.Context, r *messages.FetchCRLResponse) { select { case <-ctx.Done():