Add legacydb package to support existing MySQL DB

- add new legacydb package
- fix warnings
This commit is contained in:
Jan Dittberner 2024-01-12 19:07:24 +01:00
parent eb92755ef6
commit a6317c82c5
8 changed files with 1437 additions and 67 deletions

View file

@ -1,5 +1,5 @@
/*
Copyright 2022 CAcert Inc.
Copyright CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -31,6 +31,10 @@ import (
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
"git.cacert.org/cacert-gosigner/pkg/protocol"
"git.cacert.org/cacert-gosignerclient/internal/legacydb"
"git.cacert.org/cacert-gosignerclient/internal/client"
"git.cacert.org/cacert-gosignerclient/internal/config"
"git.cacert.org/cacert-gosignerclient/internal/handler"
@ -103,6 +107,8 @@ func generateDefaultConfig() error {
serial:
device: /dev/ttyUSB0
baud: 112500
database:
dsn: "user:password@/dbname"
`
cfg, err := config.LoadConfiguration(strings.NewReader(defaultBaseConfiguration))
@ -141,6 +147,18 @@ func startClient(configFile string, logger *logrus.Logger) error {
defer func() { _ = signerClient.Close() }()
commands := make(chan *protocol.Command, clientConfig.CommandChannelCapacity)
legacyDB, err := legacydb.New(logger, &clientConfig.Database, commands)
if err != nil {
return fmt.Errorf("could not initialize legacy database: %w", err)
}
defer func() { _ = legacyDB.Close() }()
signerClient.RegisterCommandSource(legacyDB)
signerClient.RegisterResponseSink(legacyDB)
logger.Info("setup complete, starting client operation")
ctx, cancel := context.WithCancel(context.Background())
@ -157,14 +175,14 @@ func startClient(configFile string, logger *logrus.Logger) error {
cancel()
}()
callbacks := make(chan interface{}, client.CallBackBufferSize)
callbacks := make(chan any, client.CallBackBufferSize)
clientHandler, err := handler.New(clientConfig, logger, callbacks)
if err != nil {
return fmt.Errorf("could not setup client handler: %w", err)
}
if err = signerClient.Run(ctx, callbacks, clientHandler); err != nil {
if err = signerClient.Run(ctx, callbacks, clientHandler, commands); err != nil {
return fmt.Errorf("error in client: %w", err)
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2022 CAcert Inc.
Copyright CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -82,21 +82,24 @@ type Client struct {
config *config.ClientConfig
signerInfo *SignerInfo
knownCACertificates map[string]*CACertificateInfo
commandSources []CommandSource
responseSinks map[messages.ResponseCode]ResponseSink
sync.Mutex
}
func (c *Client) Run(
ctx context.Context, callback <-chan interface{}, handler protocol.ClientHandler,
ctx context.Context, callback <-chan any, handler protocol.ClientHandler,
commands chan *protocol.Command,
) error {
const componentCount = 4
protocolErrors, framerErrors := make(chan error), make(chan error)
protocolErrors, framerErrors, sourceErrors := make(chan error), make(chan error), make(chan error)
subCtx, cancel := context.WithCancel(ctx)
wg := sync.WaitGroup{}
wg.Add(componentCount)
wg.Add(len(c.commandSources))
commands := make(chan *protocol.Command, c.config.CommandChannelCapacity)
fromSigner := make(chan []byte)
toSigner := make(chan []byte)
@ -107,6 +110,8 @@ func (c *Client) Run(
c.logger.Info("shutdown complete")
}()
c.RunSources(subCtx, &wg, sourceErrors)
go func(f protocol.Framer) {
defer wg.Done()
@ -171,6 +176,12 @@ func (c *Client) Run(
return fmt.Errorf("error from protocol: %w", err)
}
return nil
case err := <-sourceErrors:
if err != nil {
return fmt.Errorf("error from command source: %w", err)
}
return nil
}
}
@ -205,10 +216,15 @@ func (c *Client) Close() error {
type commandGenerator func(context.Context, chan<- *protocol.Command) error
func (c *Client) commandLoop(ctx context.Context, commands chan *protocol.Command, callback <-chan interface{}) {
func (c *Client) commandLoop(ctx context.Context, commands chan *protocol.Command, callback <-chan any) {
healthTimer := time.NewTimer(c.config.HealthStart)
fetchCRLTimer := time.NewTimer(c.config.FetchCRLStart)
nextCommands := make(chan *protocol.Command)
defer func() {
close(commands)
c.logger.Info("command loop stopped")
}()
for {
select {
@ -216,57 +232,54 @@ func (c *Client) commandLoop(ctx context.Context, commands chan *protocol.Comman
return
case callbackData := <-callback:
go func() {
err := c.handleCallback(ctx, nextCommands, callbackData)
err := c.handleCallback(ctx, commands, callbackData)
if err != nil {
c.logger.WithError(err).Error("callback handling failed")
}
}()
case <-fetchCRLTimer.C:
go c.scheduleRequiredCRLFetches(ctx, nextCommands)
go c.scheduleRequiredCRLFetches(ctx, commands)
fetchCRLTimer.Reset(c.config.FetchCRLInterval)
case <-healthTimer.C:
go c.scheduleHealthCheck(ctx, nextCommands)
go c.scheduleHealthCheck(ctx, commands)
healthTimer.Reset(c.config.HealthInterval)
case nextCommand, ok := <-nextCommands:
if !ok {
return
}
commands <- nextCommand
c.logger.WithFields(map[string]interface{}{
"command": nextCommand.Announce,
"buffer length": len(commands),
}).Trace("sent command")
}
}
}
type ErrNoResponseSink struct {
msg string
}
func (e ErrNoResponseSink) Error() string {
return fmt.Sprintf("no response sink for %s response found", e.msg)
}
func (c *Client) handleCallback(
ctx context.Context,
newCommands chan<- *protocol.Command,
data interface{},
data any,
) error {
var handler commandGenerator
var (
handler commandGenerator
err error
)
switch d := data.(type) {
case SignerInfo:
handler = c.updateSignerInfo(d)
case *messages.CAInfoResponse:
handler = c.updateCAInformation(d)
case *messages.FetchCRLResponse:
handler = c.updateCRL(d)
case *protocol.Response:
handler, err = c.handleResponse(d)
if err != nil {
return err
}
default:
return fmt.Errorf("unknown callback data of type %T", data)
return fmt.Errorf("unknown callback data of type %T", d)
}
if err := handler(ctx, newCommands); err != nil {
return err
}
return nil
return handler(ctx, newCommands)
}
func (c *Client) updateSignerInfo(
@ -609,6 +622,94 @@ func (c *Client) setLastKnownCRL(caName string, number *big.Int) {
caInfo.LastKnownCRL = number
}
type CommandSource interface {
Run(context.Context) error
}
type ResponseSink interface {
SupportedResponses() []messages.ResponseCode
HandleResponse(context.Context, *messages.ResponseAnnounce, any) error
NotifyError(ctx context.Context, requestID, message string) error
}
func (c *Client) RegisterCommandSource(source CommandSource) {
c.commandSources = append(c.commandSources, source)
}
func (c *Client) RegisterResponseSink(sink ResponseSink) {
for _, code := range sink.SupportedResponses() {
c.responseSinks[code] = sink
}
}
func (c *Client) handleResponse(r *protocol.Response) (commandGenerator, error) {
var handler commandGenerator
switch payload := r.Response.(type) {
case *messages.CAInfoResponse:
handler = c.updateCAInformation(payload)
case *messages.FetchCRLResponse:
handler = c.updateCRL(payload)
case *messages.ErrorResponse:
handler = func(ctx context.Context, _ chan<- *protocol.Command) error {
for _, sink := range c.responseSinks {
if err := sink.NotifyError(ctx, r.Announce.ID, payload.Message); err != nil {
return fmt.Errorf("error from response sink: %w", err)
}
}
return nil
}
case *messages.SignCertificateResponse:
sink, ok := c.responseSinks[messages.RespSignCertificate]
if !ok {
return nil, ErrNoResponseSink{"sign certificate"}
}
handler = func(ctx context.Context, _ chan<- *protocol.Command) error {
if err := sink.HandleResponse(ctx, r.Announce, payload); err != nil {
return fmt.Errorf("error from response sink: %w", err)
}
return nil
}
case *messages.SignOpenPGPResponse:
sink, ok := c.responseSinks[messages.RespSignOpenPGP]
if !ok {
return nil, ErrNoResponseSink{"sign openpgp"}
}
handler = func(ctx context.Context, _ chan<- *protocol.Command) error {
if err := sink.HandleResponse(ctx, r.Announce, payload); err != nil {
return fmt.Errorf("error from response sink: %w", err)
}
return nil
}
default:
return nil, fmt.Errorf("unhandled response %s", payload)
}
return handler, nil
}
func (c *Client) RunSources(ctx context.Context, wg *sync.WaitGroup, errorChan chan error) {
for _, source := range c.commandSources {
go func(s CommandSource) {
defer wg.Done()
err := s.Run(ctx)
if err != nil {
c.logger.WithError(err).Error("command source failed")
errorChan <- err
}
c.logger.Info("command source stopped")
}(source)
}
}
func New(
cfg *config.ClientConfig,
logger *logrus.Logger,
@ -623,6 +724,8 @@ func New(
framer: cobsFramer,
config: cfg,
knownCACertificates: make(map[string]*CACertificateInfo),
responseSinks: make(map[messages.ResponseCode]ResponseSink),
commandSources: make([]CommandSource, 0),
}
err = client.setupConnection(&serial.Config{

View file

@ -1,5 +1,5 @@
/*
Copyright 2022 CAcert Inc.
Copyright 2022-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -36,6 +36,9 @@ const (
defaultResponseDataTimeout = 2 * time.Second
defaultFilesDirectory = "public"
defaultCommandChannelCapacity = 100
defaultDatabaseConnMaxLiveTime = 3 * time.Minute
defaultDatabaseMaxOpenConns = 10
defaultDatabaseMaxIdleConns = 10
)
type SettingsError struct {
@ -52,6 +55,13 @@ type Serial struct {
Timeout time.Duration `yaml:"timeout"`
}
type Database struct {
DSN string `yaml:"dsn"`
ConnMaxLiveTime time.Duration `yaml:"conn-max-live-time"`
MaxOpenConns int `yaml:"max-open-conns"`
MaxIdleConns int `yaml:"max-idle-conns"`
}
type ClientConfig struct {
Serial Serial `yaml:"serial"`
HealthInterval time.Duration `yaml:"health-interval"`
@ -63,6 +73,7 @@ type ClientConfig struct {
PublicCRLDirectory string `yaml:"public-crl-directory"`
PublicCertificateDirectory string `yaml:"public-certificate-directory"`
CommandChannelCapacity int `yaml:"command-channel-capacity"`
Database Database `yaml:"database"`
}
func (c *ClientConfig) UnmarshalYAML(n *yaml.Node) error {
@ -77,6 +88,7 @@ func (c *ClientConfig) UnmarshalYAML(n *yaml.Node) error {
PublicCRLDirectory string `yaml:"public-crl-directory"`
PublicCertificateDirectory string `yaml:"public-certificate-directory"`
CommandChannelCapacity int `yaml:"command-channel-capacity"`
Database Database `yaml:"database"`
}{}
err := n.Decode(&data)
@ -84,16 +96,8 @@ func (c *ClientConfig) UnmarshalYAML(n *yaml.Node) error {
return fmt.Errorf("could not decode YAML: %w", err)
}
if data.Serial.Device == "" {
return SettingsError{"you must specify a serial 'device'"}
}
if data.Serial.Baud == 0 {
data.Serial.Baud = 115200
}
if data.Serial.Timeout == 0 {
data.Serial.Timeout = defaultSerialTimeout
if err := checkSerialConfig(&data.Serial); err != nil {
return err
}
c.Serial = data.Serial
@ -152,6 +156,50 @@ func (c *ClientConfig) UnmarshalYAML(n *yaml.Node) error {
c.PublicCertificateDirectory = data.PublicCRLDirectory
if err := checkDatabaseConfig(&data.Database); err != nil {
return err
}
c.Database = data.Database
c.Database = data.Database
return nil
}
func checkDatabaseConfig(d *Database) error {
if d.DSN == "" {
return SettingsError{"you must specify a database 'dsn'"}
}
if d.ConnMaxLiveTime == 0 {
d.ConnMaxLiveTime = defaultDatabaseConnMaxLiveTime
}
if d.MaxOpenConns == 0 {
d.MaxOpenConns = defaultDatabaseMaxOpenConns
}
if d.MaxIdleConns == 0 {
d.MaxIdleConns = defaultDatabaseMaxIdleConns
}
return nil
}
func checkSerialConfig(s *Serial) error {
if s.Device == "" {
return SettingsError{"you must specify a serial 'device'"}
}
if s.Baud == 0 {
s.Baud = 115200
}
if s.Timeout == 0 {
s.Timeout = defaultSerialTimeout
}
return nil
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2022 CAcert Inc.
Copyright 2022-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
@ -37,7 +37,7 @@ import (
type SignerClientHandler struct {
logger *logrus.Logger
config *config.ClientConfig
clientCallback chan<- interface{}
clientCallback chan<- any
}
var errInputClosed = errors.New("input channel has been closed")
@ -155,16 +155,13 @@ func (s *SignerClientHandler) HandleResponse(ctx context.Context, response *prot
s.logger.WithField("response", response).Debug("full response")
switch r := response.Response.(type) {
case *messages.ErrorResponse:
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:
s.logger.WithField("response", response).Warnf("unhandled response of type %T", response.Response)
s.logger.WithField("response", response).Tracef(
"delegate response handling of type %T", response.Response,
)
s.handleGenericResponse(ctx, response)
}
return nil
@ -215,20 +212,11 @@ func (s *SignerClientHandler) handleHealthResponse(ctx context.Context, r *messa
}
}
func (s *SignerClientHandler) handleCAInfoResponse(ctx context.Context, r *messages.CAInfoResponse) {
func (s *SignerClientHandler) handleGenericResponse(ctx context.Context, response *protocol.Response) {
select {
case <-ctx.Done():
return
case s.clientCallback <- r:
break
}
}
func (s *SignerClientHandler) handleFetchCRLResponse(ctx context.Context, r *messages.FetchCRLResponse) {
select {
case <-ctx.Done():
return
case s.clientCallback <- r:
case s.clientCallback <- response:
break
}
}
@ -236,7 +224,7 @@ func (s *SignerClientHandler) handleFetchCRLResponse(ctx context.Context, r *mes
func New(
config *config.ClientConfig,
logger *logrus.Logger,
clientCallback chan interface{},
clientCallback chan any,
) (protocol.ClientHandler, error) {
return &SignerClientHandler{
logger: logger,

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,186 @@
/*
Copyright 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 legacydb
import (
"crypto/x509"
"crypto/x509/pkix"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_extractSubjectParts(t *testing.T) {
tests := []struct {
name string
subject string
want *x509.Certificate
wantErr bool
}{
{
"personal user subject",
"/CN=John Doe/emailAddress=john.doe@example.org",
&x509.Certificate{
Subject: pkix.Name{CommonName: "John Doe"},
EmailAddresses: []string{"john.doe@example.org"},
},
false,
},
{
"subject with supported and unsupported alt names",
"/CN=a.example.com/subjectAltName=DNS:a.example.com/" +
"subjectAltName=otherName:1.3.6.1.5.5.7.8.5;UTF8:a.example.com",
&x509.Certificate{
Subject: pkix.Name{CommonName: "a.example.com"},
DNSNames: []string{"a.example.com"},
},
false,
},
{
"subject with ISO-8859-1 special characters",
"/CN=D\xf6ner Kebap/emailAddress=doener@example.org",
&x509.Certificate{
Subject: pkix.Name{CommonName: "Döner Kebap"},
EmailAddresses: []string{"doener@example.org"},
},
false,
},
{
"subject with Windows1252 special characters",
"/CN=J\xe1no\x9a Test\x9c/emailAddress=janos.testoe@example.org",
&x509.Certificate{
Subject: pkix.Name{CommonName: "Jánoš Testœ"},
EmailAddresses: []string{"janos.testoe@example.org"},
},
false,
},
{
"WoT User subject",
"/CN=CAcert WoT User/emailAddress=test@example.org",
&x509.Certificate{
Subject: pkix.Name{CommonName: "CAcert WoT User"},
EmailAddresses: []string{"test@example.org"},
},
false,
},
{
"Keep address order",
"/CN=CAcert WoT User/emailAddress=wot.user@example.com/emailAddress=wu@example.com",
&x509.Certificate{
Subject: pkix.Name{CommonName: "CAcert WoT User"},
EmailAddresses: []string{"wot.user@example.com", "wu@example.com"},
},
false,
},
{
"Keep DNS name order",
"/CN=Test User/subjectAltName=DNS:www.example.com/subjectAltName=DNS:example.com",
&x509.Certificate{
Subject: pkix.Name{CommonName: "Test User"},
DNSNames: []string{"www.example.com", "example.com"},
},
false,
},
{
"Organization user without OU",
"/CN=Test User/emailAddress=test@example.org/organizationName=Acme Inc./" +
"localityName=Example town/stateOrProvinceName=BW/countryName=DE",
&x509.Certificate{
Subject: pkix.Name{
CommonName: "Test User",
Organization: []string{"Acme Inc."},
Locality: []string{"Example town"},
Province: []string{"BW"},
Country: []string{"DE"},
},
EmailAddresses: []string{"test@example.org"},
},
false,
},
{
"Organization user with OU",
"/CN=Test User/emailAddress=test@example.org/organizationalUnitName=IT/" +
"organizationName=Acme Inc./localityName=Example town/countryName=DE",
&x509.Certificate{
Subject: pkix.Name{
CommonName: "Test User",
Organization: []string{"Acme Inc."},
OrganizationalUnit: []string{"IT"},
Locality: []string{"Example town"},
Country: []string{"DE"},
},
EmailAddresses: []string{"test@example.org"},
},
false,
},
{
"Organization domain without OU",
"/organizationName=Acme Inc./localityName=Example Town/stateOrProvinceName=BW/countryName=DE/" +
"commonName=www.example.org",
&x509.Certificate{
Subject: pkix.Name{CommonName: "www.example.org",
Organization: []string{"Acme Inc."},
Locality: []string{"Example Town"},
Province: []string{"BW"},
Country: []string{"DE"},
},
DNSNames: []string{"www.example.org"},
},
false,
},
{
"Organization domain with OU",
"/organizationalUnitName=IT/organizationName=Acme Inc./localityName=Example Town/" +
"stateOrProvinceName=BW/countryName=DE/commonName=example.org",
&x509.Certificate{
Subject: pkix.Name{CommonName: "example.org",
Organization: []string{"Acme Inc."},
OrganizationalUnit: []string{"IT"},
Locality: []string{"Example Town"},
Province: []string{"BW"},
Country: []string{"DE"},
},
DNSNames: []string{"example.org"},
},
false,
},
{
"Empty subject",
"",
nil,
true,
},
{
"No = in part",
"/CNexample",
nil,
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := extractSubjectParts(tt.subject)
if (err != nil) != tt.wantErr {
t.Errorf("extractSubjectParts() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.Equal(t, tt.want, got, "extractSubjectParts() got = %v, want %v", got, tt.want)
})
}
}

View file

@ -0,0 +1,12 @@
Your CAcert signed key for {{ .Email }} is available online at:
https://www.cacert.org/gpg.php?id=3&cert={{ .RowID }}
To help improve the trust of CAcert in general, it's appreciated if you could also sign our key and upload it to a key
server. Below is a copy of our primary key details:
pub 1024D/65D0FD58 2003-07-11 CA Cert Signing Authority (Root CA) <gpg@cacert.org>
Key fingerprint = A31D 4F81 EF4E BD07 B456 FA04 D2BB 0D01 65D0 FD58
Best regards
CAcert.org Support!

View file

@ -0,0 +1 @@
[CAcert.org] Your GPG/PGP Key