Jan Dittberner
afe7d23c9b
This commit defines command codes for planned commands and response codes for their corresponding responses. The health response from the HSM access component has been reduced to avoid unnecessary data transmissions. A new CA information command has been implemented. This command can be used to retrieve the CA certificate and profile information for a given CA name. The client simulator has been updated to retrieve CA information for all CAs when the list of CAs changes.
367 lines
8.6 KiB
Go
367 lines
8.6 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.
|
|
*/
|
|
|
|
// client simulator
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/shamaton/msgpackgen/msgpack"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"git.cacert.org/cacert-gosigner/pkg/protocol"
|
|
|
|
"git.cacert.org/cacert-gosigner/pkg/messages"
|
|
)
|
|
|
|
type TestCommandGenerator struct {
|
|
logger *logrus.Logger
|
|
commands chan *protocol.Command
|
|
}
|
|
|
|
func (g *TestCommandGenerator) GenerateCommands(ctx context.Context) error {
|
|
// write some leading garbage to test signer robustness
|
|
_, _ = io.CopyN(os.Stdout, rand.Reader, 50) //nolint:gomnd
|
|
|
|
g.commands <- &protocol.Command{
|
|
Announce: messages.BuildCommandAnnounce(messages.CmdHealth),
|
|
Command: &messages.HealthCommand{},
|
|
}
|
|
|
|
const (
|
|
healthInterval = 5 * time.Second
|
|
crlInterval = 15 * time.Minute
|
|
startPause = 3 * time.Second
|
|
)
|
|
|
|
g.logger.Info("start generating commands")
|
|
|
|
time.Sleep(startPause)
|
|
|
|
g.commands <- &protocol.Command{
|
|
Announce: messages.BuildCommandAnnounce(messages.CmdFetchCRL),
|
|
Command: &messages.FetchCRLCommand{IssuerID: "sub-ecc_person_2022"},
|
|
}
|
|
|
|
healthTimer := time.NewTimer(healthInterval)
|
|
crlTimer := time.NewTimer(crlInterval)
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
_ = healthTimer.Stop()
|
|
|
|
g.logger.Info("stopped health check loop")
|
|
|
|
_ = crlTimer.Stop()
|
|
|
|
g.logger.Info("stopped CRL fetch loop")
|
|
|
|
return nil
|
|
case <-healthTimer.C:
|
|
g.commands <- &protocol.Command{
|
|
Announce: messages.BuildCommandAnnounce(messages.CmdHealth),
|
|
Command: &messages.HealthCommand{},
|
|
}
|
|
|
|
healthTimer.Reset(healthInterval)
|
|
case <-crlTimer.C:
|
|
g.commands <- &protocol.Command{
|
|
Announce: messages.BuildCommandAnnounce(messages.CmdFetchCRL),
|
|
Command: &messages.FetchCRLCommand{IssuerID: "sub-ecc_person_2022"},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type clientSimulator struct {
|
|
clientHandler protocol.ClientHandler
|
|
framesIn chan []byte
|
|
framesOut chan []byte
|
|
framer protocol.Framer
|
|
commandGenerator *TestCommandGenerator
|
|
logger *logrus.Logger
|
|
}
|
|
|
|
const (
|
|
responseAnnounceTimeout = 30 * time.Second
|
|
responseDataTimeout = 2 * time.Second
|
|
)
|
|
|
|
func (c *clientSimulator) Run(ctx context.Context) error {
|
|
framerErrors := make(chan error)
|
|
protocolErrors := make(chan error)
|
|
generatorErrors := make(chan error)
|
|
|
|
go func() {
|
|
err := c.framer.ReadFrames(ctx, os.Stdin, c.framesIn)
|
|
|
|
framerErrors <- err
|
|
}()
|
|
|
|
go func() {
|
|
err := c.framer.WriteFrames(ctx, os.Stdout, c.framesOut)
|
|
|
|
framerErrors <- err
|
|
}()
|
|
|
|
go func() {
|
|
clientProtocol := protocol.NewClient(c.clientHandler, c.commandGenerator.commands, c.framesIn, c.framesOut, c.logger)
|
|
|
|
err := clientProtocol.Handle(ctx)
|
|
|
|
protocolErrors <- err
|
|
}()
|
|
|
|
go func() {
|
|
err := c.commandGenerator.GenerateCommands(ctx)
|
|
|
|
generatorErrors <- err
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil
|
|
case err := <-framerErrors:
|
|
if err != nil {
|
|
return fmt.Errorf("error from framer: %w", err)
|
|
}
|
|
|
|
return nil
|
|
case err := <-generatorErrors:
|
|
if err != nil {
|
|
return fmt.Errorf("error from command generator: %w", err)
|
|
}
|
|
|
|
return nil
|
|
case err := <-protocolErrors:
|
|
if err != nil {
|
|
return fmt.Errorf("error from protocol handler: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
type ClientHandler struct {
|
|
logger *logrus.Logger
|
|
commands chan *protocol.Command
|
|
caList []string
|
|
}
|
|
|
|
func (c *ClientHandler) Send(ctx context.Context, command *protocol.Command, out chan []byte) error {
|
|
var (
|
|
frame []byte
|
|
err error
|
|
)
|
|
|
|
frame, err = msgpack.Marshal(command.Announce)
|
|
if err != nil {
|
|
return fmt.Errorf("could not marshal command annoucement: %w", err)
|
|
}
|
|
|
|
c.logger.WithField("announcement", command.Announce).Info("write command announcement")
|
|
|
|
c.logger.Trace("writing command announcement")
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil
|
|
case out <- frame:
|
|
break
|
|
}
|
|
|
|
frame, err = msgpack.Marshal(command.Command)
|
|
if err != nil {
|
|
return fmt.Errorf("could not marshal command data: %w", err)
|
|
}
|
|
|
|
c.logger.WithField("command", command.Command).Info("write command data")
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil
|
|
case out <- frame:
|
|
break
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *ClientHandler) ResponseAnnounce(ctx context.Context, in chan []byte) (*protocol.Response, error) {
|
|
response := &protocol.Response{}
|
|
|
|
var announce messages.ResponseAnnounce
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, nil
|
|
case frame := <-in:
|
|
if err := msgpack.Unmarshal(frame, &announce); err != nil {
|
|
return nil, fmt.Errorf("could not unmarshal response announcement: %w", err)
|
|
}
|
|
|
|
response.Announce = &announce
|
|
|
|
c.logger.WithField("announcement", response.Announce).Debug("received response announcement")
|
|
|
|
return response, nil
|
|
case <-time.After(responseAnnounceTimeout):
|
|
return nil, protocol.ErrResponseAnnounceTimeoutExpired
|
|
}
|
|
}
|
|
|
|
func (c *ClientHandler) ResponseData(ctx context.Context, in chan []byte, response *protocol.Response) error {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil
|
|
case frame := <-in:
|
|
switch response.Announce.Code {
|
|
case messages.RespHealth:
|
|
var resp messages.HealthResponse
|
|
if err := msgpack.Unmarshal(frame, &resp); err != nil {
|
|
return fmt.Errorf("could not unmarshal health response data: %w", err)
|
|
}
|
|
|
|
c.updateCAs(ctx, c.commands, resp.Info)
|
|
|
|
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)
|
|
}
|
|
|
|
certificate, err := x509.ParseCertificate(resp.Certificate)
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse certificate data: %w", err)
|
|
}
|
|
|
|
c.logger.Infof(
|
|
"certificate for %s: subject=%s, issuer=%s, serial=0x%x, valid from %s to %s",
|
|
resp.Name,
|
|
certificate.Subject,
|
|
certificate.Issuer,
|
|
certificate.SerialNumber,
|
|
certificate.NotBefore,
|
|
certificate.NotAfter)
|
|
|
|
response.Response = &resp
|
|
case messages.RespFetchCRL:
|
|
var resp messages.FetchCRLResponse
|
|
if err := msgpack.Unmarshal(frame, &resp); err != nil {
|
|
return fmt.Errorf("could not unmarshal fetch CRL response data: %w", err)
|
|
}
|
|
|
|
response.Response = &resp
|
|
default:
|
|
return fmt.Errorf("unhandled response code %s", response.Announce.Code)
|
|
}
|
|
case <-time.After(responseDataTimeout):
|
|
return protocol.ErrResponseDataTimeoutExpired
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *ClientHandler) HandleResponse(_ context.Context, response *protocol.Response) error {
|
|
c.logger.WithField("response", response.Announce).Info("handled response")
|
|
c.logger.WithField("response", response).Debug("full response")
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *ClientHandler) updateCAs(ctx context.Context, out chan *protocol.Command, info []*messages.HealthInfo) {
|
|
caList := make([]string, 0)
|
|
|
|
for _, i := range info {
|
|
if i.Source == "HSM" {
|
|
c.logger.Debugf("info from HSM: %s", i)
|
|
|
|
for caName := range i.MoreInfo {
|
|
caList = append(caList, caName)
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Strings(caList)
|
|
|
|
if len(caList) != len(c.caList) {
|
|
c.caList = caList
|
|
|
|
for _, ca := range c.caList {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case out <- &protocol.Command{
|
|
Announce: messages.BuildCommandAnnounce(messages.CmdCAInfo),
|
|
Command: &messages.CAInfoCommand{Name: ca},
|
|
}:
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func newClientHandler(logger *logrus.Logger, commands chan *protocol.Command) *ClientHandler {
|
|
return &ClientHandler{logger: logger, commands: commands}
|
|
}
|
|
|
|
func main() {
|
|
logger := logrus.New()
|
|
logger.SetOutput(os.Stderr)
|
|
logger.SetLevel(logrus.DebugLevel)
|
|
|
|
messages.RegisterGeneratedResolver()
|
|
|
|
cobsFramer, err := protocol.NewCOBSFramer(logger)
|
|
if err != nil {
|
|
logger.WithError(err).Fatal("could not create COBS framer")
|
|
}
|
|
|
|
commandBufferSize := 50
|
|
|
|
commandCh := make(chan *protocol.Command, commandBufferSize)
|
|
|
|
sim := &clientSimulator{
|
|
commandGenerator: &TestCommandGenerator{
|
|
logger: logger,
|
|
commands: commandCh,
|
|
},
|
|
logger: logger,
|
|
framesIn: make(chan []byte),
|
|
framesOut: make(chan []byte),
|
|
framer: cobsFramer,
|
|
clientHandler: newClientHandler(logger, commandCh),
|
|
}
|
|
|
|
err = sim.Run(context.Background())
|
|
if err != nil {
|
|
logger.WithError(err).Error("simulator returned an error")
|
|
}
|
|
}
|