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).
This commit is contained in:
Jan Dittberner 2022-12-03 11:52:32 +01:00
parent 199f0ee0c0
commit da24ae70b6
7 changed files with 339 additions and 121 deletions

2
.gitignore vendored
View file

@ -1,8 +1,8 @@
*Pty
/.idea/
/config.yaml
/crls/
/dist/
/public/
/signerclient
go.work
go.work.sum

2
go.mod
View file

@ -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

6
go.sum
View file

@ -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=

View file

@ -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) {
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()
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: lastKnown})
infos = append(infos, CRLInfo{Name: caInfo.Name, LastKnown: c.lastKnownCRL(caInfo)})
}
}
return infos
}
func (c *Client) lastKnownCRL(caName string) *big.Int {
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")
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
return nil, nil
}
c.lastKnownCRLS[d.IssuerID] = crlNumber
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 := 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{

View file

@ -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{},
}
}

View file

@ -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
}

View file

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