cacert-gosignerclient/internal/client/client.go
Jan Dittberner c65853d1f9 Implement config generator
This commit adds code to allow the generation of a default client
configuration. The generator is run instead of the regular client
code, when the option -generate-config is passed on the command
line.
2022-12-03 12:22:00 +01:00

561 lines
12 KiB
Go

/*
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 client
import (
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"math/big"
"os"
"path"
"sync"
"time"
"github.com/balacode/go-delta"
"github.com/sirupsen/logrus"
"github.com/tarm/serial"
"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"
)
const CallBackBufferSize = 50
const (
worldReadableDirPerm = 0o755
worldReadableFilePerm = 0o644
)
type Profile struct {
Name string
UseFor string
}
type CertInfo struct {
Name string
FetchCert bool
FetchCRL bool
LastKnownCRL *big.Int
Certificate *x509.Certificate
Profiles map[string]*Profile
}
type SignerInfo struct {
SignerHealth bool
SignerVersion string
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
knownCertificates map[string]*CertInfo
callback chan interface{}
sync.Mutex
}
func (c *Client) Run(ctx context.Context) error {
protocolErrors := make(chan error)
framerErrors := make(chan error)
go func(f protocol.Framer) {
framerErrors <- f.ReadFrames(ctx, c.port, c.in)
}(c.framer)
go func(f protocol.Framer) {
framerErrors <- f.WriteFrames(ctx, c.port, c.out)
}(c.framer)
go func() {
clientProtocol := protocol.NewClient(c.handler, c.commands, c.in, c.out, c.logger)
protocolErrors <- clientProtocol.Handle(ctx)
}()
ctx, cancelCommandLoop := context.WithCancel(ctx)
go c.commandLoop(ctx)
for {
select {
case <-ctx.Done():
cancelCommandLoop()
return nil
case err := <-framerErrors:
cancelCommandLoop()
if err != nil {
return fmt.Errorf("error from framer: %w", err)
}
return nil
case err := <-protocolErrors:
cancelCommandLoop()
if err != nil {
return fmt.Errorf("error from protocol: %w", err)
}
return nil
}
}
}
func (c *Client) setupConnection(serialConfig *serial.Config) error {
s, err := serial.OpenPort(serialConfig)
if err != nil {
return fmt.Errorf("could not open serial port: %w", err)
}
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
}
func (c *Client) Close() error {
close(c.in)
close(c.out)
if c.port != nil {
err := c.port.Close()
if err != nil {
return fmt.Errorf("could not close serial port: %w", err)
}
}
return nil
}
func (c *Client) commandLoop(ctx context.Context) {
healthTimer := time.NewTimer(c.config.HealthStart)
fetchCRLTimer := time.NewTimer(c.config.FetchCRLStart)
for {
newCommands := make([]*protocol.Command, 0)
select {
case <-ctx.Done():
return
case callbackData := <-c.callback:
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.requiredCRLs() {
newCommands = append(newCommands, command.FetchCRL(crlInfo.Name, crlInfo.LastKnown))
}
fetchCRLTimer.Reset(c.config.FetchCRLInterval)
case <-healthTimer.C:
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{}) ([]*protocol.Command, error) {
switch d := data.(type) {
case SignerInfo:
return c.updateSignerInfo(d)
case *messages.CAInfoResponse:
return c.updateCAInformation(d)
case *messages.FetchCRLResponse:
return c.updateCRL(d)
default:
return nil, fmt.Errorf("unknown callback data of type %T", data)
}
}
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) updateCAInformation(d *messages.CAInfoResponse) ([]*protocol.Command, error) {
c.Lock()
defer c.Unlock()
caInfo, ok := c.knownCertificates[d.Name]
if !ok {
c.logger.WithField("certificate", d.Name).Warn("unknown CA certificate")
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 {
Name string
LastKnown *big.Int
}
func (c *Client) requiredCRLs() []CRLInfo {
c.Lock()
defer c.Unlock()
if c.knownCertificates == nil {
c.logger.Warn("no certificates known")
return nil
}
infos := make([]CRLInfo, 0)
for _, caInfo := range c.knownCertificates {
if caInfo.FetchCRL {
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)
for _, caInfo := range c.knownCertificates {
if caInfo.FetchCert {
infos = append(infos, caInfo.Name)
}
}
return infos
}
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")
return nil
}
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")
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
}
return lastKnown
}
func (c *Client) updateCRL(d *messages.FetchCRLResponse) ([]*protocol.Command, error) {
var (
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 nil, nil
}
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 nil, nil
}
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")
return nil, nil
}
list, err := x509.ParseRevocationList(der)
if err != nil {
c.logger.WithError(err).Error("could not parse patched CRL")
return nil, nil
}
crlNumber = list.Number
}
if err := c.writeCRL(d.IssuerID, der); err != nil {
c.logger.WithError(err).Error("could not store CRL")
caInfo.LastKnownCRL = nil
return nil, nil
}
caInfo.LastKnownCRL = crlNumber
return nil, nil
}
func (c *Client) buildCRLFileName(caName string) string {
return path.Join(c.config.PublicCRLDirectory, fmt.Sprintf("%s.crl", caName))
}
func (c *Client) buildCertificateFileName(caName string, certFormat string) string {
return path.Join(c.config.PublicCRLDirectory, fmt.Sprintf("%s.%s", caName, certFormat))
}
func (c *Client) writeCertificate(caName string, derBytes []byte) error {
if err := os.MkdirAll(c.config.PublicCRLDirectory, worldReadableDirPerm); err != nil {
return fmt.Errorf("could not create public CA data directory %s: %w", c.config.PublicCRLDirectory, 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.PublicCRLDirectory, worldReadableDirPerm); err != nil {
return fmt.Errorf("could not create public CA data directory %s: %w", c.config.PublicCRLDirectory, 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 (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,
handler protocol.ClientHandler,
commands chan *protocol.Command,
callback chan interface{},
) (*Client, error) {
cobsFramer, err := protocol.NewCOBSFramer(logger)
if err != nil {
return nil, fmt.Errorf("could not create COBS framer: %w", err)
}
client := &Client{
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{
Name: cfg.Serial.Device,
Baud: cfg.Serial.Baud,
ReadTimeout: cfg.Serial.Timeout,
})
if err != nil {
return nil, err
}
return client, nil
}