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 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -31,6 +31,10 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3" "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/client"
"git.cacert.org/cacert-gosignerclient/internal/config" "git.cacert.org/cacert-gosignerclient/internal/config"
"git.cacert.org/cacert-gosignerclient/internal/handler" "git.cacert.org/cacert-gosignerclient/internal/handler"
@ -103,6 +107,8 @@ func generateDefaultConfig() error {
serial: serial:
device: /dev/ttyUSB0 device: /dev/ttyUSB0
baud: 112500 baud: 112500
database:
dsn: "user:password@/dbname"
` `
cfg, err := config.LoadConfiguration(strings.NewReader(defaultBaseConfiguration)) cfg, err := config.LoadConfiguration(strings.NewReader(defaultBaseConfiguration))
@ -141,6 +147,18 @@ func startClient(configFile string, logger *logrus.Logger) error {
defer func() { _ = signerClient.Close() }() 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") logger.Info("setup complete, starting client operation")
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
@ -157,14 +175,14 @@ func startClient(configFile string, logger *logrus.Logger) error {
cancel() cancel()
}() }()
callbacks := make(chan interface{}, client.CallBackBufferSize) callbacks := make(chan any, client.CallBackBufferSize)
clientHandler, err := handler.New(clientConfig, logger, callbacks) clientHandler, err := handler.New(clientConfig, logger, callbacks)
if err != nil { if err != nil {
return fmt.Errorf("could not setup client handler: %w", err) 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) 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 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -82,21 +82,24 @@ type Client struct {
config *config.ClientConfig config *config.ClientConfig
signerInfo *SignerInfo signerInfo *SignerInfo
knownCACertificates map[string]*CACertificateInfo knownCACertificates map[string]*CACertificateInfo
commandSources []CommandSource
responseSinks map[messages.ResponseCode]ResponseSink
sync.Mutex sync.Mutex
} }
func (c *Client) Run( 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 { ) error {
const componentCount = 4 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) subCtx, cancel := context.WithCancel(ctx)
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
wg.Add(componentCount) wg.Add(componentCount)
wg.Add(len(c.commandSources))
commands := make(chan *protocol.Command, c.config.CommandChannelCapacity)
fromSigner := make(chan []byte) fromSigner := make(chan []byte)
toSigner := make(chan []byte) toSigner := make(chan []byte)
@ -107,6 +110,8 @@ func (c *Client) Run(
c.logger.Info("shutdown complete") c.logger.Info("shutdown complete")
}() }()
c.RunSources(subCtx, &wg, sourceErrors)
go func(f protocol.Framer) { go func(f protocol.Framer) {
defer wg.Done() defer wg.Done()
@ -171,6 +176,12 @@ func (c *Client) Run(
return fmt.Errorf("error from protocol: %w", err) 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 return nil
} }
} }
@ -205,10 +216,15 @@ func (c *Client) Close() error {
type commandGenerator func(context.Context, chan<- *protocol.Command) 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) healthTimer := time.NewTimer(c.config.HealthStart)
fetchCRLTimer := time.NewTimer(c.config.FetchCRLStart) fetchCRLTimer := time.NewTimer(c.config.FetchCRLStart)
nextCommands := make(chan *protocol.Command)
defer func() {
close(commands)
c.logger.Info("command loop stopped")
}()
for { for {
select { select {
@ -216,57 +232,54 @@ func (c *Client) commandLoop(ctx context.Context, commands chan *protocol.Comman
return return
case callbackData := <-callback: case callbackData := <-callback:
go func() { go func() {
err := c.handleCallback(ctx, nextCommands, callbackData) err := c.handleCallback(ctx, commands, callbackData)
if err != nil { if err != nil {
c.logger.WithError(err).Error("callback handling failed") c.logger.WithError(err).Error("callback handling failed")
} }
}() }()
case <-fetchCRLTimer.C: case <-fetchCRLTimer.C:
go c.scheduleRequiredCRLFetches(ctx, nextCommands) go c.scheduleRequiredCRLFetches(ctx, commands)
fetchCRLTimer.Reset(c.config.FetchCRLInterval) fetchCRLTimer.Reset(c.config.FetchCRLInterval)
case <-healthTimer.C: case <-healthTimer.C:
go c.scheduleHealthCheck(ctx, nextCommands) go c.scheduleHealthCheck(ctx, commands)
healthTimer.Reset(c.config.HealthInterval) healthTimer.Reset(c.config.HealthInterval)
case nextCommand, ok := <-nextCommands:
if !ok {
return
} }
}
}
commands <- nextCommand type ErrNoResponseSink struct {
msg string
}
c.logger.WithFields(map[string]interface{}{ func (e ErrNoResponseSink) Error() string {
"command": nextCommand.Announce, return fmt.Sprintf("no response sink for %s response found", e.msg)
"buffer length": len(commands),
}).Trace("sent command")
}
}
} }
func (c *Client) handleCallback( func (c *Client) handleCallback(
ctx context.Context, ctx context.Context,
newCommands chan<- *protocol.Command, newCommands chan<- *protocol.Command,
data interface{}, data any,
) error { ) error {
var handler commandGenerator var (
handler commandGenerator
err error
)
switch d := data.(type) { switch d := data.(type) {
case SignerInfo: case SignerInfo:
handler = c.updateSignerInfo(d) handler = c.updateSignerInfo(d)
case *messages.CAInfoResponse: case *protocol.Response:
handler = c.updateCAInformation(d) handler, err = c.handleResponse(d)
case *messages.FetchCRLResponse: if err != nil {
handler = c.updateCRL(d)
default:
return fmt.Errorf("unknown callback data of type %T", data)
}
if err := handler(ctx, newCommands); err != nil {
return err return err
} }
default:
return fmt.Errorf("unknown callback data of type %T", d)
}
return nil return handler(ctx, newCommands)
} }
func (c *Client) updateSignerInfo( func (c *Client) updateSignerInfo(
@ -609,6 +622,94 @@ func (c *Client) setLastKnownCRL(caName string, number *big.Int) {
caInfo.LastKnownCRL = number 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( func New(
cfg *config.ClientConfig, cfg *config.ClientConfig,
logger *logrus.Logger, logger *logrus.Logger,
@ -623,6 +724,8 @@ func New(
framer: cobsFramer, framer: cobsFramer,
config: cfg, config: cfg,
knownCACertificates: make(map[string]*CACertificateInfo), knownCACertificates: make(map[string]*CACertificateInfo),
responseSinks: make(map[messages.ResponseCode]ResponseSink),
commandSources: make([]CommandSource, 0),
} }
err = client.setupConnection(&serial.Config{ 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 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -36,6 +36,9 @@ const (
defaultResponseDataTimeout = 2 * time.Second defaultResponseDataTimeout = 2 * time.Second
defaultFilesDirectory = "public" defaultFilesDirectory = "public"
defaultCommandChannelCapacity = 100 defaultCommandChannelCapacity = 100
defaultDatabaseConnMaxLiveTime = 3 * time.Minute
defaultDatabaseMaxOpenConns = 10
defaultDatabaseMaxIdleConns = 10
) )
type SettingsError struct { type SettingsError struct {
@ -52,6 +55,13 @@ type Serial struct {
Timeout time.Duration `yaml:"timeout"` 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 { type ClientConfig struct {
Serial Serial `yaml:"serial"` Serial Serial `yaml:"serial"`
HealthInterval time.Duration `yaml:"health-interval"` HealthInterval time.Duration `yaml:"health-interval"`
@ -63,6 +73,7 @@ type ClientConfig struct {
PublicCRLDirectory string `yaml:"public-crl-directory"` PublicCRLDirectory string `yaml:"public-crl-directory"`
PublicCertificateDirectory string `yaml:"public-certificate-directory"` PublicCertificateDirectory string `yaml:"public-certificate-directory"`
CommandChannelCapacity int `yaml:"command-channel-capacity"` CommandChannelCapacity int `yaml:"command-channel-capacity"`
Database Database `yaml:"database"`
} }
func (c *ClientConfig) UnmarshalYAML(n *yaml.Node) error { 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"` PublicCRLDirectory string `yaml:"public-crl-directory"`
PublicCertificateDirectory string `yaml:"public-certificate-directory"` PublicCertificateDirectory string `yaml:"public-certificate-directory"`
CommandChannelCapacity int `yaml:"command-channel-capacity"` CommandChannelCapacity int `yaml:"command-channel-capacity"`
Database Database `yaml:"database"`
}{} }{}
err := n.Decode(&data) 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) return fmt.Errorf("could not decode YAML: %w", err)
} }
if data.Serial.Device == "" { if err := checkSerialConfig(&data.Serial); err != nil {
return SettingsError{"you must specify a serial 'device'"} return err
}
if data.Serial.Baud == 0 {
data.Serial.Baud = 115200
}
if data.Serial.Timeout == 0 {
data.Serial.Timeout = defaultSerialTimeout
} }
c.Serial = data.Serial c.Serial = data.Serial
@ -152,6 +156,50 @@ func (c *ClientConfig) UnmarshalYAML(n *yaml.Node) error {
c.PublicCertificateDirectory = data.PublicCRLDirectory 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 return nil
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2022 CAcert Inc. Copyright 2022-2023 CAcert Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -37,7 +37,7 @@ import (
type SignerClientHandler struct { type SignerClientHandler struct {
logger *logrus.Logger logger *logrus.Logger
config *config.ClientConfig config *config.ClientConfig
clientCallback chan<- interface{} clientCallback chan<- any
} }
var errInputClosed = errors.New("input channel has been closed") 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") s.logger.WithField("response", response).Debug("full response")
switch r := response.Response.(type) { switch r := response.Response.(type) {
case *messages.ErrorResponse:
s.logger.WithField("message", r.Message).Error("error from signer")
case *messages.HealthResponse: case *messages.HealthResponse:
s.handleHealthResponse(ctx, r) s.handleHealthResponse(ctx, r)
case *messages.CAInfoResponse:
s.handleCAInfoResponse(ctx, r)
case *messages.FetchCRLResponse:
s.handleFetchCRLResponse(ctx, r)
default: 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 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 { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case s.clientCallback <- r: case s.clientCallback <- response:
break
}
}
func (s *SignerClientHandler) handleFetchCRLResponse(ctx context.Context, r *messages.FetchCRLResponse) {
select {
case <-ctx.Done():
return
case s.clientCallback <- r:
break break
} }
} }
@ -236,7 +224,7 @@ func (s *SignerClientHandler) handleFetchCRLResponse(ctx context.Context, r *mes
func New( func New(
config *config.ClientConfig, config *config.ClientConfig,
logger *logrus.Logger, logger *logrus.Logger,
clientCallback chan interface{}, clientCallback chan any,
) (protocol.ClientHandler, error) { ) (protocol.ClientHandler, error) {
return &SignerClientHandler{ return &SignerClientHandler{
logger: logger, 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