406 lines
8.4 KiB
Go
406 lines
8.4 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"
|
|
"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/config"
|
|
)
|
|
|
|
const CallBackBufferSize = 50
|
|
|
|
const (
|
|
worldReadableDirPerm = 0o755
|
|
worldReadableFilePerm = 0o644
|
|
)
|
|
|
|
type Profile struct {
|
|
Name string
|
|
UseFor string
|
|
}
|
|
|
|
type CertInfo struct {
|
|
Name string
|
|
FetchCRL bool
|
|
}
|
|
|
|
type SignerInfo struct {
|
|
SignerHealth bool
|
|
SignerVersion string
|
|
CACertificates []CertInfo
|
|
UsableProfiles map[string][]Profile
|
|
}
|
|
|
|
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{}
|
|
sync.Mutex
|
|
lastKnownCRLS map[string]*big.Int
|
|
}
|
|
|
|
func (c *Client) Run(ctx context.Context) error {
|
|
protocolErrors := make(chan error)
|
|
framerErrors := make(chan error)
|
|
|
|
go func(f protocol.Framer) {
|
|
framerErrors <- f.ReadFrames(c.port, c.in)
|
|
}(c.framer)
|
|
|
|
go func(f protocol.Framer) {
|
|
framerErrors <- f.WriteFrames(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, 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 {
|
|
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() {
|
|
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,
|
|
},
|
|
}
|
|
}
|
|
|
|
fetchCRLTimer.Reset(c.config.FetchCRLInterval)
|
|
case <-healthTimer.C:
|
|
c.commands <- &protocol.Command{
|
|
Announce: messages.BuildCommandAnnounce(messages.CmdHealth),
|
|
Command: &messages.HealthCommand{},
|
|
}
|
|
|
|
healthTimer.Reset(c.config.HealthInterval)
|
|
}
|
|
}
|
|
}
|
|
|
|
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, 0)
|
|
|
|
for _, caInfo := range c.signerInfo.CACertificates {
|
|
if caInfo.FetchCRL {
|
|
lastKnown := c.lastKnownCRL(caInfo.Name)
|
|
|
|
infos = append(infos, CRLInfo{Name: caInfo.Name, 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,
|
|
framer: protocol.NewCOBSFramer(logger),
|
|
in: make(chan []byte),
|
|
out: make(chan []byte),
|
|
commands: commands,
|
|
handler: handler,
|
|
config: cfg,
|
|
callback: callback,
|
|
lastKnownCRLS: make(map[string]*big.Int),
|
|
}
|
|
|
|
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
|
|
}
|