1014 lines
27 KiB
Go
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")
|
|
}
|
|
}
|