cacert-gosignerclient/internal/legacydb/legacydb.go
2024-01-12 19:07:24 +01:00

1014 lines
27 KiB
Go

/*
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 is emulates the behavior of the old signer client by polling from a MySQL database using the old
// schema, reading CSR files from the filesystem and storing certificates in the file system.
package legacydb
import (
"bytes"
"context"
"crypto"
"crypto/rand"
"crypto/x509"
"database/sql"
"errors"
"fmt"
"math/big"
"os"
"path"
"strconv"
"strings"
"sync"
"text/template"
"time"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/packet"
_ "github.com/go-sql-driver/mysql" // imported for mysql database support
"github.com/sirupsen/logrus"
"github.com/wneessen/go-mail"
"golang.org/x/text/encoding/charmap"
"git.cacert.org/cacert-gosigner/pkg/messages"
"git.cacert.org/cacert-gosigner/pkg/protocol"
"git.cacert.org/cacert-gosignerclient/internal/config"
)
type responseType int
func (t responseType) String() string {
switch t {
case respGPG:
return "gpg"
case respPersonalClientCertificate:
return "person_client"
case respPersonalCodeSigningCertificate:
return "person_code"
case respPersonalServerCertificate:
return "person_server"
case respOrganizationalClientCertificate:
return "org_client"
case respOrganizationalCodeSigningCertificate:
return "org_code"
case respOrganizationalServerCertificate:
return "org_server"
default:
return "generic"
}
}
const (
respGPG responseType = iota
respPersonalClientCertificate
respPersonalCodeSigningCertificate
respPersonalServerCertificate
respOrganizationalClientCertificate
respOrganizationalCodeSigningCertificate
respOrganizationalServerCertificate
respPersonalClientRevoke
respPersonalServerRevoke
respOrganizationalClientRevoke
respOrganizationalServerRevoke
)
const maxRetries = 3
const (
sqlFindOpenPGPKeys = `SELECT id, email, csr
FROM gpg
WHERE crt = ''
AND csr != ''
AND warning < ?`
sqlFindPersonalClientCertRequests = `SELECT id, csr_name, type, md, subject
FROM emailcerts
WHERE crt_name = ''
AND csr_name != ''
AND codesign = 0
AND warning < ?`
sqlFindPersonalCodeSigningCertRequests = `SELECT id, csr_name, type, md, subject
FROM emailcerts
WHERE crt_name = ''
AND csr_name != ''
AND codesign = 1
AND warning < ?`
sqlFindPersonalServerCertRequests = `SELECT id, csr_name, type, md, subject
FROM domaincerts
WHERE crt_name = ''
AND csr_name != ''
AND warning < ?`
sqlFindOrganizationalClientCertRequests = `SELECT id, csr_name, type, md, subject
FROM orgemailcerts
WHERE crt_name = ''
AND csr_name != ''
AND codesign = 0
AND warning < ?`
sqlFindOrganizationalCodeSigningCertRequests = `SELECT id, csr_name, type, md, subject
FROM orgemailcerts
WHERE crt_name = ''
AND csr_name != ''
AND codesign = 1
AND warning < ?`
sqlFindOrganizationalServerCertRequests = `SELECT id, csr_name, type, md, subject
FROM orgdomaincerts
WHERE crt_name = ''
AND csr_name != ''
AND warning < ?`
sqlRecordPersonalClientCert = `UPDATE emailcerts
SET crt_name=?,
modified=CURRENT_TIMESTAMP,
serial=?,
expire=?
WHERE id = ?`
sqlRecordPersonalServerCert = `UPDATE domaincerts
SET crt_name=?,
modified=CURRENT_TIMESTAMP,
serial=?,
expire=?
WHERE id = ?`
sqlRecordOrganizationalClientCert = `UPDATE orgemailcerts
SET crt_name=?,
modified=CURRENT_TIMESTAMP,
serial=?,
expire=?
WHERE id = ?`
sqlRecordOrganizationalServerCert = `UPDATE orgdomaincerts
SET crt_name=?,
modified=CURRENT_TIMESTAMP,
serial=?,
expire=?
WHERE id = ?`
sqlRecordFailedOpenPGP = `UPDATE gpg
SET warning = warning + 1
WHERE id = ?`
sqlRecordFailedPersonalClientCertificate = `UPDATE emailcerts
SET warning = warning + 1
WHERE id = ?`
sqlRecordFailedPersonalServerCertificate = `UPDATE domaincerts
SET warning = warning + 1
WHERE id = ?`
sqlRecordFailedOrganizationalClientCertificate = `UPDATE orgemailcerts
SET warning = warning + 1
WHERE id = ?`
sqlRecordFailedOrganizationalServerCertificate = `UPDATE orgdomaincerts
SET warning = warning + 1
WHERE id = ?`
)
type pendingResponse struct {
responseType responseType
rowID int
}
type emailData struct {
responseType responseType
rowID int
}
type LegacyDB struct {
db *sql.DB
logger *logrus.Logger
commands chan *protocol.Command
emails chan emailData
issuerIDs map[responseType]string
profiles map[responseType]string
pending map[string]pendingResponse
algorithms map[string]crypto.Hash
failureQuery map[responseType]string
sync.Mutex
}
func (d *LegacyDB) NotifyError(ctx context.Context, requestID, message string) error {
pr, ok := d.pending[requestID]
if !ok {
d.logger.Debugf("ignoring unknown request id %s", requestID)
return nil
}
switch pr.responseType {
case respGPG,
respOrganizationalClientCertificate, respOrganizationalCodeSigningCertificate,
respOrganizationalServerCertificate,
respPersonalClientCertificate, respPersonalCodeSigningCertificate,
respPersonalServerCertificate:
break
case respPersonalClientRevoke, respPersonalServerRevoke,
respOrganizationalClientRevoke, respOrganizationalServerRevoke:
d.logger.WithFields(logrus.Fields{
"type": pr.responseType,
"row": pr.rowID,
"request_id": requestID,
"message": message,
}).Warn("revocation failed")
return nil
default:
return fmt.Errorf("unhandled pending response of type %s with row ID %d", pr.responseType, pr.rowID)
}
d.logger.WithFields(logrus.Fields{
"type": pr.responseType,
"row": pr.rowID,
"request_id": requestID,
"message": message,
}).Warn("request failed")
query, ok := d.failureQuery[pr.responseType]
if !ok {
return fmt.Errorf("no failure query for %s defined", pr.responseType)
}
d.recordFailure(ctx, query, pr.rowID)
delete(d.pending, requestID)
return nil
}
func (d *LegacyDB) SupportedResponses() []messages.ResponseCode {
return []messages.ResponseCode{
messages.RespSignCertificate,
messages.RespRevokeCertificate,
messages.RespSignOpenPGP,
}
}
type ErrUnsupportedResponseType struct {
response any
}
func (e ErrUnsupportedResponseType) Error() string {
return fmt.Sprintf("unsupported response type %T", e.response)
}
func (d *LegacyDB) HandleResponse(ctx context.Context, announce *messages.ResponseAnnounce, r any) error {
switch v := r.(type) {
case *messages.RevokeCertificateResponse:
if err := d.handleRevokedCertificate(ctx, announce, v); err != nil {
return err
}
case *messages.SignCertificateResponse:
if err := d.handleSignedCertificate(ctx, announce, v); err != nil {
return err
}
case *messages.SignOpenPGPResponse:
if err := d.handleSignedOpenPGPKey(ctx, announce, v); err != nil {
return err
}
default:
return ErrUnsupportedResponseType{r}
}
return nil
}
func New(logger *logrus.Logger, config *config.Database, commands chan *protocol.Command) (*LegacyDB, error) {
db, err := sql.Open("mysql", config.DSN)
if err != nil {
return nil, fmt.Errorf("could not open database: %w", err)
}
db.SetConnMaxLifetime(config.ConnMaxLiveTime)
db.SetMaxOpenConns(config.MaxOpenConns)
db.SetMaxIdleConns(config.MaxIdleConns)
const emailChanSize = 100
emails := make(chan emailData, emailChanSize)
// TODO: replace hardcoded issuerIDs with configuration or auto-detection
issuerIDs := map[responseType]string{
respGPG: "gpg",
respPersonalClientCertificate: "rsa_person_2022",
respPersonalCodeSigningCertificate: "rsa_person_2022",
respPersonalServerCertificate: "rsa_server_2022",
respOrganizationalClientCertificate: "rsa_person_2022",
respOrganizationalCodeSigningCertificate: "rsa_person_2022",
respOrganizationalServerCertificate: "rsa_server_2022",
}
// TODO: replace hardcoded profiles with configuration or auto-detection
profiles := map[responseType]string{
respGPG: "gpg",
respPersonalClientCertificate: "person",
respPersonalCodeSigningCertificate: "code",
respPersonalServerCertificate: "server",
respOrganizationalClientCertificate: "org_person",
respOrganizationalCodeSigningCertificate: "org_code",
respOrganizationalServerCertificate: "org_server",
}
failureQuery := map[responseType]string{
respGPG: sqlRecordFailedOpenPGP,
respPersonalClientCertificate: sqlRecordFailedPersonalClientCertificate,
respPersonalCodeSigningCertificate: sqlRecordFailedPersonalClientCertificate,
respPersonalServerCertificate: sqlRecordFailedPersonalServerCertificate,
respOrganizationalClientCertificate: sqlRecordFailedOrganizationalClientCertificate,
respOrganizationalCodeSigningCertificate: sqlRecordFailedOrganizationalClientCertificate,
respOrganizationalServerCertificate: sqlRecordFailedOrganizationalServerCertificate,
}
pending := make(map[string]pendingResponse)
return &LegacyDB{
db: db,
logger: logger,
commands: commands,
emails: emails,
issuerIDs: issuerIDs,
profiles: profiles,
pending: pending,
failureQuery: failureQuery,
}, nil
}
func (d *LegacyDB) Run(ctx context.Context) error { //nolint:gocognit,cyclop
const tickDuration = 2700 * time.Millisecond
subCtx, cancel := context.WithCancel(ctx)
defer func() {
d.logger.Trace("run cancel in LegacyDB source")
cancel()
}()
go func() {
for {
select {
case <-subCtx.Done():
return
case e := <-d.emails:
err := d.sendNotificationEmail(subCtx, e)
if err != nil {
d.logger.WithError(err).Warn("could not send email notification")
}
}
}
}()
nextTick := time.NewTimer(tickDuration)
for {
select {
case <-ctx.Done():
nextTick.Stop()
return nil
case <-nextTick.C:
if err := d.requestSignedOpenPGPKeys(ctx); err != nil {
return err
}
if err := d.requestCerts(
ctx, sqlFindPersonalClientCertRequests, respPersonalClientCertificate,
); err != nil {
return err
}
if err := d.requestCerts(
ctx, sqlFindPersonalCodeSigningCertRequests, respPersonalCodeSigningCertificate,
); err != nil {
return err
}
if err := d.requestCerts(
ctx, sqlFindPersonalServerCertRequests, respPersonalServerCertificate,
); err != nil {
return err
}
if err := d.requestCerts(
ctx, sqlFindOrganizationalClientCertRequests, respOrganizationalClientCertificate,
); err != nil {
return err
}
if err := d.requestCerts(
ctx, sqlFindOrganizationalCodeSigningCertRequests, respOrganizationalCodeSigningCertificate,
); err != nil {
return err
}
if err := d.requestCerts(
ctx, sqlFindOrganizationalServerCertRequests, respOrganizationalServerCertificate,
); err != nil {
return err
}
if err := d.revokePersonalClientCerts(ctx); err != nil {
return err
}
if err := d.revokePersonalServerCerts(ctx); err != nil {
return err
}
if err := d.revokeOrganizationClientCerts(ctx); err != nil {
return err
}
if err := d.revokeOrganizationServerCerts(ctx); err != nil {
return err
}
nextTick.Reset(tickDuration)
}
}
}
func (d *LegacyDB) Close() error {
err := d.db.Close()
if err != nil {
return fmt.Errorf("could not close database connection pool: %w", err)
}
return nil
}
func (d *LegacyDB) requestSignedOpenPGPKeys(ctx context.Context) error {
rows, err := d.db.QueryContext(ctx, sqlFindOpenPGPKeys, maxRetries)
if err != nil {
return fmt.Errorf("could not execute query: %w", err)
}
defer func() { _ = rows.Close() }()
idsWithIssues := make([]int, 0)
for rows.Next() {
if err := rows.Err(); err != nil {
return fmt.Errorf("could not fetch row: %w", err)
}
var (
csrID int
email string
pubKeyFileName string
)
if err := rows.Scan(&csrID, &email, &pubKeyFileName); err != nil {
return fmt.Errorf("could not scan row: %w", err)
}
keyBytes, err := os.ReadFile(pubKeyFileName)
if err != nil {
d.logger.WithFields(logrus.Fields{
"id": csrID,
"file_name": pubKeyFileName,
}).WithError(err).Warn("could not read public key data")
idsWithIssues = append(idsWithIssues, csrID)
continue
}
command := &protocol.Command{
Announce: messages.BuildCommandAnnounce(messages.CmdSignOpenPGP),
Command: messages.SignOpenPGPCommand{
IssuerID: d.issuerIDs[respGPG],
ProfileName: d.profiles[respGPG],
PublicKey: keyBytes,
CommonName: "",
EmailAddresses: []string{email},
},
}
d.Lock()
d.pending[command.Announce.ID] = pendingResponse{respGPG, csrID}
d.Unlock()
d.commands <- command
}
if len(idsWithIssues) > 0 {
d.logger.WithFields(logrus.Fields{
"ids_with_issues": idsWithIssues,
"rt": respGPG,
}).Warn("some certificates failed")
for _, id := range idsWithIssues {
d.recordFailure(ctx, sqlRecordFailedOpenPGP, id)
}
}
return nil
}
func (d *LegacyDB) requestCerts(ctx context.Context, query string, rt responseType) error {
issuerID, ok := d.issuerIDs[rt]
if !ok {
return fmt.Errorf("no known issuer id for type %s", rt)
}
profileID, ok := d.profiles[rt]
if !ok {
return fmt.Errorf("no known profile id for type %s", rt)
}
rows, err := d.db.QueryContext(ctx, query, maxRetries)
if err != nil {
return fmt.Errorf("could not execute query: %w", err)
}
defer func() { _ = rows.Close() }()
idsWithIssues := make([]int, 0)
for rows.Next() {
if err := rows.Err(); err != nil {
return fmt.Errorf("could not fetch row: %w", err)
}
var (
csrID int
csrFileName string
certType int
md string
subject string
)
if err := rows.Scan(&csrID, &csrFileName, &certType, &md, &subject); err != nil {
return fmt.Errorf("could not scan row: %w", err)
}
csrBytes, err := os.ReadFile(csrFileName)
if err != nil {
d.logger.WithFields(logrus.Fields{
"id": csrID,
"file_name": csrFileName,
}).WithError(err).Warn("could not read CSR data")
idsWithIssues = append(idsWithIssues, csrID)
continue
}
hashAlg, ok := d.algorithms[md]
if !ok {
d.logger.WithFields(logrus.Fields{}).Warn("unsupported hash algorithm")
idsWithIssues = append(idsWithIssues, csrID)
continue
}
subjParts, err := extractSubjectParts(subject)
if err != nil {
d.logger.WithFields(logrus.Fields{
"id": csrID,
"subject": subject,
}).WithError(err).Warn("could not parse subject")
idsWithIssues = append(idsWithIssues, csrID)
continue
}
command := &protocol.Command{
Announce: messages.BuildCommandAnnounce(messages.CmdSignCertificate),
Command: messages.SignCertificateCommand{
IssuerID: issuerID,
ProfileName: profileID,
CSRData: csrBytes,
CommonName: subjParts.Subject.CommonName,
Organization: subjParts.Subject.Organization[0],
OrganizationalUnit: subjParts.Subject.OrganizationalUnit[0],
Locality: subjParts.Subject.Locality[0],
Province: subjParts.Subject.Province[0],
Country: subjParts.Subject.Country[0],
Hostnames: subjParts.DNSNames,
EmailAddresses: subjParts.EmailAddresses,
PreferredHash: hashAlg,
},
}
d.Lock()
d.pending[command.Announce.ID] = pendingResponse{rt, csrID}
d.Unlock()
d.commands <- command
}
if len(idsWithIssues) > 0 {
d.logger.WithFields(logrus.Fields{
"ids_with_issues": idsWithIssues,
"rt": rt,
}).Warn("some certificates failed")
}
return nil
}
var (
errInvalidSANPart = errors.New("invalid SAN part, missing colon")
errUnsupportedSubjectPart = errors.New("unsupported subject part")
)
// extractSubjectParts splits an openssl style subject string into subject DN parts and subject alternative parts of
// different types
func extractSubjectParts(subject string) (*x509.Certificate, error) {
if len(subject) == 0 {
return nil, errors.New("empty subject")
}
legacyCharset := charmap.Windows1252
latin1Decoder := legacyCharset.NewDecoder()
utf8Subject, err := latin1Decoder.String(subject)
if err != nil {
return nil, fmt.Errorf("subject could not be decoded from %s", legacyCharset)
}
parts := strings.Split(utf8Subject, "/")
if len(parts) == 0 {
return nil, errors.New("no subject parts in subject")
}
res := &x509.Certificate{}
for _, p := range parts {
if p == "" {
continue
}
pieces := strings.SplitN(p, "=", 2)
if len(pieces) != 2 {
return nil, fmt.Errorf("missing '=' in %s", p)
}
err := parseSubjectStringComponent(pieces[0], pieces[1], res)
if err != nil {
return nil, fmt.Errorf("could not parse subject part %s: %w", p, err)
}
}
return res, nil
}
func parseSubjectStringComponent(identifier, value string, res *x509.Certificate) error {
switch identifier {
case "CN":
res.Subject.CommonName = value
case "commonName":
res.Subject.CommonName = value
res.DNSNames = append(res.DNSNames, value)
case "organizationName":
res.Subject.Organization = append(res.Subject.Organization, value)
case "organizationalUnitName":
res.Subject.OrganizationalUnit = append(res.Subject.OrganizationalUnit, value)
case "localityName":
res.Subject.Locality = append(res.Subject.Locality, value)
case "stateOrProvinceName":
res.Subject.Province = append(res.Subject.Province, value)
case "countryName":
res.Subject.Country = append(res.Subject.Country, value)
case "emailAddress":
res.EmailAddresses = append(res.EmailAddresses, value)
case "subjectAltName":
sanParts := strings.SplitN(value, ":", 2)
if len(sanParts) != 2 {
return errInvalidSANPart
}
if sanParts[0] == "DNS" {
res.DNSNames = append(res.DNSNames, sanParts[1])
}
default:
return errUnsupportedSubjectPart
}
return nil
}
func (d *LegacyDB) revokePersonalClientCerts(_ context.Context) error {
panic("not implemented")
}
func (d *LegacyDB) revokePersonalServerCerts(_ context.Context) error {
panic("not implemented")
}
func (d *LegacyDB) revokeOrganizationClientCerts(_ context.Context) error {
panic("not implemented")
}
func (d *LegacyDB) revokeOrganizationServerCerts(_ context.Context) error {
panic("not implemented")
}
func (d *LegacyDB) writeCertificate(prefix string, rowID int, signatureData []byte) (string, error) {
crtFileName := path.Join("..", "crt", prefix, strconv.Itoa(rowID/1000), fmt.Sprintf("%s-%d.crt", prefix, rowID))
err := os.WriteFile(crtFileName, signatureData, 0o644) //nolint:gosec,gomnd
if err != nil {
return "", fmt.Errorf("could not write to file: %w", err)
}
return crtFileName, nil
}
func (d *LegacyDB) recordCertificate(_ context.Context, query string, rowID int, certBytes []byte) error {
panic(fmt.Sprintf(
"not implemented: record certificate with query %s for rowID %d (%d bytes)",
query, rowID, len(certBytes),
))
}
func (d *LegacyDB) recordPersonalClientRevoke(_ context.Context, rowID int, revokedAt time.Time) error {
panic(fmt.Sprintf(
"not implemented: record personal client certificate revocation for id %d at %s",
rowID, revokedAt,
))
}
func (d *LegacyDB) recordPersonalServerRevoke(_ context.Context, rowID int, revokedAt time.Time) error {
panic(fmt.Sprintf(
"not implemented: record personal server certificate revocation for id %d at %s",
rowID, revokedAt,
))
}
func (d *LegacyDB) recordOrganizationalClientRevoke(_ context.Context, rowID int, revokedAt time.Time) error {
panic(fmt.Sprintf(
"not implemented record organizational client certificate revocation for id %d at %s",
rowID, revokedAt,
))
}
func (d *LegacyDB) recordOrganizationalServerRevoke(_ context.Context, rowID int, revokedAt time.Time) error {
panic(fmt.Sprintf(
"not implemented record organizational server certificate revocation for id %d at %s",
rowID, revokedAt,
))
}
func (d *LegacyDB) recordSignedOpenPGPKey(ctx context.Context, rowID int, signatureData []byte) error {
packetReader := packet.NewReader(bytes.NewReader(signatureData))
entity, err := openpgp.ReadEntity(packetReader)
if err != nil {
return fmt.Errorf("could not read OpenPGP packets: %w", err)
}
lifeTimeSecs := entity.Subkeys[0].Sig.SigLifetimeSecs
if lifeTimeSecs == nil {
return errors.New("signature does not expire")
}
gpgExpiry := entity.Subkeys[0].Sig.CreationTime.Add(time.Second * time.Duration(*lifeTimeSecs))
crtFileName, err := d.writeCertificate("gpg", rowID, signatureData)
if err != nil {
return fmt.Errorf("could not write OpenPGP result: %w", err)
}
_, err = d.db.ExecContext(
ctx,
`UPDATE gpg SET crt=?, issued=NOW(), expire=? WHERE id=?`,
crtFileName, gpgExpiry, rowID,
)
if err != nil {
return fmt.Errorf("could not execute query: %w", err)
}
d.emails <- emailData{respGPG, rowID}
return nil
}
func (d *LegacyDB) handleRevokedCertificate(
ctx context.Context,
announce *messages.ResponseAnnounce,
v *messages.RevokeCertificateResponse,
) error {
d.Lock()
defer d.Unlock()
pending, ok := d.pending[announce.ID]
if !ok {
return fmt.Errorf("could not find pending request for id %s", announce.ID)
}
var err error
switch pending.responseType {
case respPersonalClientRevoke:
err = d.recordPersonalClientRevoke(ctx, pending.rowID, v.RevokedAt)
case respPersonalServerRevoke:
err = d.recordPersonalServerRevoke(ctx, pending.rowID, v.RevokedAt)
case respOrganizationalClientRevoke:
err = d.recordOrganizationalClientRevoke(ctx, pending.rowID, v.RevokedAt)
case respOrganizationalServerRevoke:
err = d.recordOrganizationalServerRevoke(ctx, pending.rowID, v.RevokedAt)
default:
return fmt.Errorf("unexpected response type for pending request %s", announce.ID)
}
delete(d.pending, announce.ID)
return err
}
func (d *LegacyDB) handleSignedCertificate(
ctx context.Context,
announce *messages.ResponseAnnounce,
r *messages.SignCertificateResponse,
) error {
d.Lock()
defer d.Unlock()
pending, ok := d.pending[announce.ID]
if !ok {
return fmt.Errorf("could not find pending request for id %s", announce.ID)
}
var err error
switch pending.responseType {
case respPersonalClientCertificate, respPersonalCodeSigningCertificate:
err = d.recordCertificate(ctx, sqlRecordPersonalClientCert, pending.rowID, r.CertificateData)
case respPersonalServerCertificate:
err = d.recordCertificate(ctx, sqlRecordPersonalServerCert, pending.rowID, r.CertificateData)
case respOrganizationalClientCertificate, respOrganizationalCodeSigningCertificate:
err = d.recordCertificate(ctx, sqlRecordOrganizationalClientCert, pending.rowID, r.CertificateData)
case respOrganizationalServerCertificate:
err = d.recordCertificate(ctx, sqlRecordOrganizationalServerCert, pending.rowID, r.CertificateData)
default:
return fmt.Errorf("unexpected response type for pending request %s", announce.ID)
}
delete(d.pending, announce.ID)
return err
}
func (d *LegacyDB) handleSignedOpenPGPKey(
ctx context.Context,
announce *messages.ResponseAnnounce,
v *messages.SignOpenPGPResponse,
) error {
d.Lock()
defer d.Unlock()
pending, ok := d.pending[announce.ID]
if !ok {
return fmt.Errorf("could not find pending request for id %s", announce.ID)
}
if pending.responseType != respGPG {
return fmt.Errorf("unexpected response type for pending request %s", announce.ID)
}
err := d.recordSignedOpenPGPKey(ctx, pending.rowID, v.SignatureData)
delete(d.pending, announce.ID)
return err
}
type templateData struct {
RowID int
Email string
}
func (d *LegacyDB) sendNotificationEmail(ctx context.Context, e emailData) error {
var (
subjectTemplate, mailTemplate *template.Template
err error
)
language, data, err := d.getTemplateData(ctx, e)
if err != nil {
return err
}
if subjectTemplate, err = d.getSubjectTemplate(e.responseType, language); err != nil {
return err
}
if mailTemplate, err = d.getEmailTemplate(e.responseType, language); err != nil {
return err
}
subject := new(bytes.Buffer)
if err = subjectTemplate.Execute(subject, data); err != nil {
return fmt.Errorf("could not build email subject: %w", err)
}
rn, err := rand.Int(rand.Reader, big.NewInt(int64(1<<31))) //nolint:gomnd
if err != nil {
return fmt.Errorf("could not get random number for unique sender addresse: %w", err)
}
// TODO: make sender address configurable
m := mail.NewMsg()
if err = m.EnvelopeFrom(fmt.Sprintf("noreply+%d@cacert.org", rn)); err != nil {
return fmt.Errorf("could not set ENVELOPE FROM address: %w", err)
}
m.SetMessageID()
m.SetDate()
m.Subject(subject.String())
if err = m.SetBodyTextTemplate(mailTemplate, data); err != nil {
return fmt.Errorf("could not set text template for email body: %w", err)
}
c, err := mail.NewClient("localhost")
if err != nil {
return fmt.Errorf("could not create mail client: %w", err)
}
if err = c.DialAndSendWithContext(ctx, m); err != nil {
return fmt.Errorf("failed to deliver mail: %w", err)
}
return nil
}
func (d *LegacyDB) getTemplateData(ctx context.Context, e emailData) (string, *templateData, error) {
var (
userID int
userLanguage string
)
data := &templateData{RowID: e.rowID}
if e.responseType == respGPG {
row := d.db.QueryRowContext(ctx, `SELECT memid, email FROM gpg WHERE id=?`, e.rowID)
if err := row.Err(); err != nil {
return "", nil, fmt.Errorf("could not fetch row: %w", err)
}
if err := row.Scan(&userID, &data.Email); err != nil {
return "", nil, fmt.Errorf("could not scan row: %w", err)
}
}
row := d.db.QueryRowContext(ctx, `SELECT language FROM users WHERE id=?`, userID)
if err := row.Err(); err != nil {
return "", nil, fmt.Errorf("could not fetch row: %w", err)
}
if err := row.Scan(&userLanguage); err != nil {
return "", nil, fmt.Errorf("could not scan row: %w", err)
}
return userLanguage, data, nil
}
func (d *LegacyDB) getLocalizedTemplate(language string, fileName string) (*template.Template, error) {
templateFile := path.Join("templates", "mail", language, fileName)
_, err := os.Stat(templateFile)
if errors.Is(err, os.ErrNotExist) {
templateFile = path.Join("templates", "mail", "en", fileName)
}
t, err := template.ParseFiles(templateFile)
if err != nil {
return nil, fmt.Errorf("could not parse template: %w", err)
}
return t, nil
}
func (d *LegacyDB) getSubjectTemplate(rt responseType, language string) (*template.Template, error) {
fileName := fmt.Sprintf("%s_subject.txt", rt.String())
return d.getLocalizedTemplate(language, fileName)
}
func (d *LegacyDB) getEmailTemplate(rt responseType, language string) (*template.Template, error) {
fileName := fmt.Sprintf("%s_body.txt", rt.String())
return d.getLocalizedTemplate(language, fileName)
}
func (d *LegacyDB) recordFailure(ctx context.Context, query string, rowID int) {
if _, err := d.db.ExecContext(ctx, query, rowID); err != nil {
d.logger.WithError(err).Error("database update failed")
}
}