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():