Jan Dittberner
da24ae70b6
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).
569 lines
12 KiB
Go
569 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.PublicDataDirectory, fmt.Sprintf("%s.crl", caName))
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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 := c.ensurePublicDataDirectory(); err != nil {
|
|
return 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
|
|
}
|