Jan Dittberner
ad6b987c91
- decouple config and messages - cainfo maps from config.Profile to messages.CAProfile - config parses profile usage - validity can be configured per certificate profile, defaults are defined in a defaultValidity method of the profile usage - the client simulator emits certificate signing requests at random intervals - add implementation of SingCertificateCommand to MsgPackHandler - remove indirection signing.RequestSignature
411 lines
9.9 KiB
Go
411 lines
9.9 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"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"fmt"
|
|
"io"
|
|
mathRand "math/rand"
|
|
"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
|
|
minSignInterval = 5 * time.Second
|
|
maxSignInterval = 10 * time.Second
|
|
)
|
|
|
|
g.logger.Info("start generating commands")
|
|
|
|
time.Sleep(startPause)
|
|
|
|
g.commands <- &protocol.Command{
|
|
Announce: messages.BuildCommandAnnounce(messages.CmdFetchCRL),
|
|
Command: &messages.FetchCRLCommand{IssuerID: "ecc_person_2022"},
|
|
}
|
|
|
|
healthTimer := time.NewTimer(healthInterval)
|
|
crlTimer := time.NewTimer(crlInterval)
|
|
signTimer := time.NewTimer(minSignInterval)
|
|
|
|
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: "ecc_person_2022"},
|
|
}
|
|
case <-signTimer.C:
|
|
g.commands <- &protocol.Command{
|
|
Announce: messages.BuildCommandAnnounce(messages.CmdSignCertificate),
|
|
Command: &messages.SignCertificateCommand{
|
|
IssuerID: "ecc_person_2022",
|
|
ProfileName: "person",
|
|
CSRData: g.generateCsr("Test Person"),
|
|
CommonName: "Test Person",
|
|
EmailAddresses: []string{"test@example.org"},
|
|
PreferredHash: crypto.SHA256,
|
|
},
|
|
}
|
|
|
|
newRandomDuration := minSignInterval + time.Duration(mathRand.Int63n(int64(maxSignInterval)))
|
|
|
|
signTimer.Reset(newRandomDuration)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *TestCommandGenerator) generateCsr(cn string) []byte {
|
|
keyPair, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
g.logger.WithError(err).Panic("could not generate key pair")
|
|
}
|
|
|
|
template := &x509.CertificateRequest{
|
|
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
|
PublicKey: keyPair.Public(),
|
|
Subject: pkix.Name{CommonName: cn},
|
|
}
|
|
|
|
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, template, keyPair)
|
|
if err != nil {
|
|
g.logger.WithError(err).Panic("could not create signing request")
|
|
}
|
|
|
|
return csrBytes
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|