From 73aad9d74e3c96d6e33d4bac12af7f70e02b0d58 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Fri, 12 Jan 2024 20:48:13 +0100 Subject: [PATCH] Continue certificate signing implementation - parse PEM CSRs to DER before sending them to the signer - handle SignCertificate and SignOpenPGP response messages - add file system prefix constants for writing certificate files - define supported hash algorithms - fix handling of signing requests without organization, organizational unit, or location information - use a different SMTP port for debugging with mailpit --- internal/handler/handler.go | 16 ++++- internal/legacydb/legacydb.go | 126 ++++++++++++++++++++++++++-------- 2 files changed, 111 insertions(+), 31 deletions(-) diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 1d7aaf7..9aa73df 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -1,5 +1,5 @@ /* -Copyright 2022-2023 CAcert Inc. +Copyright CAcert Inc. SPDX-License-Identifier: Apache-2.0 Licensed under the Apache License, Version 2.0 (the "License"); @@ -132,6 +132,20 @@ func (s *SignerClientHandler) ResponseData(ctx context.Context, in <-chan []byte return fmt.Errorf("could not unmarshal fetch CRL response data: %w", err) } + response.Response = &resp + case messages.RespSignCertificate: + var resp messages.SignCertificateResponse + if err := msgpack.Unmarshal(frame, &resp); err != nil { + return fmt.Errorf("could not unmarshal sign certificate response data: %w", err) + } + + response.Response = &resp + case messages.RespSignOpenPGP: + var resp messages.SignOpenPGPResponse + if err := msgpack.Unmarshal(frame, &resp); err != nil { + return fmt.Errorf("could not unmarshal sign OpenPGP response data: %w", err) + } + response.Response = &resp case messages.RespError: var resp messages.ErrorResponse diff --git a/internal/legacydb/legacydb.go b/internal/legacydb/legacydb.go index 6a95fa4..b343096 100644 --- a/internal/legacydb/legacydb.go +++ b/internal/legacydb/legacydb.go @@ -15,7 +15,7 @@ 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 +// Package legacydb 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 @@ -26,6 +26,7 @@ import ( "crypto/rand" "crypto/x509" "database/sql" + "encoding/pem" "errors" "fmt" "math/big" @@ -172,6 +173,14 @@ SET warning = warning + 1 WHERE id = ?` ) +const ( + prefixOpenPGP = "gpg" + prefixPersonalClient = "client" + prefixPersonalServer = "server" + prefixOrganizationalClient = "orgclient" + prefixOrganizationalServer = "orgserver" +) + type pendingResponse struct { responseType responseType rowID int @@ -315,6 +324,12 @@ func New(logger *logrus.Logger, config *config.Database, commands chan *protocol respOrganizationalServerCertificate: "org_server", } + supportedHashAlgorithms := map[string]crypto.Hash{ + "sha256": crypto.SHA256, + "sha384": crypto.SHA384, + "sha512": crypto.SHA512, + } + failureQuery := map[responseType]string{ respGPG: sqlRecordFailedOpenPGP, respPersonalClientCertificate: sqlRecordFailedPersonalClientCertificate, @@ -332,6 +347,7 @@ func New(logger *logrus.Logger, config *config.Database, commands chan *protocol logger: logger, commands: commands, emails: emails, + algorithms: supportedHashAlgorithms, issuerIDs: issuerIDs, profiles: profiles, pending: pending, @@ -540,7 +556,7 @@ func (d *LegacyDB) requestCerts(ctx context.Context, query string, rt responseTy var ( csrID int csrFileName string - certType int + certType sql.NullInt16 md string subject string ) @@ -549,7 +565,7 @@ func (d *LegacyDB) requestCerts(ctx context.Context, query string, rt responseTy return fmt.Errorf("could not scan row: %w", err) } - csrBytes, err := os.ReadFile(csrFileName) + pemBytes, err := os.ReadFile(csrFileName) if err != nil { d.logger.WithFields(logrus.Fields{ "id": csrID, @@ -561,6 +577,23 @@ func (d *LegacyDB) requestCerts(ctx context.Context, query string, rt responseTy continue } + csrBlock, remaining := pem.Decode(pemBytes) + if len(remaining) > 0 { + d.logger.WithFields(logrus.Fields{"id": csrID, "file_name": csrFileName}).Warn("unhandled CSR bytes") + + idsWithIssues = append(idsWithIssues, csrID) + + continue + } + + if csrBlock.Type != "CERTIFICATE REQUEST" { + d.logger.WithFields(logrus.Fields{"id": csrID, "file_name": csrFileName, "pem_block_type": csrBlock.Type}).Warn("unhandled PEM block type") + + idsWithIssues = append(idsWithIssues, csrID) + + continue + } + hashAlg, ok := d.algorithms[md] if !ok { d.logger.WithFields(logrus.Fields{}).Warn("unsupported hash algorithm") @@ -582,22 +615,11 @@ func (d *LegacyDB) requestCerts(ctx context.Context, query string, rt responseTy continue } + signCertCommand := buildSignCertificateCommand(issuerID, profileID, csrBlock.Bytes, subjParts, hashAlg) + 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, - }, + Command: signCertCommand, } d.Lock() @@ -617,6 +639,45 @@ func (d *LegacyDB) requestCerts(ctx context.Context, query string, rt responseTy return nil } +func buildSignCertificateCommand( + issuerID string, + profileID string, + csrBytes []byte, + subjParts *x509.Certificate, + hashAlg crypto.Hash, +) messages.SignCertificateCommand { + signCertCommand := messages.SignCertificateCommand{ + IssuerID: issuerID, + ProfileName: profileID, + CSRData: csrBytes, + CommonName: subjParts.Subject.CommonName, + Hostnames: subjParts.DNSNames, + EmailAddresses: subjParts.EmailAddresses, + PreferredHash: hashAlg, + } + + if len(subjParts.Subject.Organization) > 0 { + signCertCommand.Organization = subjParts.Subject.Organization[0] + } + + if len(subjParts.Subject.OrganizationalUnit) > 0 { + signCertCommand.OrganizationalUnit = subjParts.Subject.OrganizationalUnit[0] + } + + if len(subjParts.Subject.Locality) > 0 { + signCertCommand.Locality = subjParts.Subject.Locality[0] + } + + if len(subjParts.Subject.Province) > 0 { + signCertCommand.Province = subjParts.Subject.Province[0] + } + + if len(subjParts.Subject.Country) > 0 { + signCertCommand.Country = subjParts.Subject.Country[0] + } + return signCertCommand +} + var ( errInvalidSANPart = errors.New("invalid SAN part, missing colon") errUnsupportedSubjectPart = errors.New("unsupported subject part") @@ -702,19 +763,23 @@ func parseSubjectStringComponent(identifier, value string, res *x509.Certificate } func (d *LegacyDB) revokePersonalClientCerts(_ context.Context) error { - panic("not implemented") + logrus.Debug("not implemented") + return nil } func (d *LegacyDB) revokePersonalServerCerts(_ context.Context) error { - panic("not implemented") + logrus.Debug("not implemented") + return nil } func (d *LegacyDB) revokeOrganizationClientCerts(_ context.Context) error { - panic("not implemented") + logrus.Debug("not implemented") + return nil } func (d *LegacyDB) revokeOrganizationServerCerts(_ context.Context) error { - panic("not implemented") + logrus.Debug("not implemented") + return nil } func (d *LegacyDB) writeCertificate(prefix string, rowID int, signatureData []byte) (string, error) { @@ -728,10 +793,10 @@ func (d *LegacyDB) writeCertificate(prefix string, rowID int, signatureData []by return crtFileName, nil } -func (d *LegacyDB) recordCertificate(_ context.Context, query string, rowID int, certBytes []byte) error { +func (d *LegacyDB) recordCertificate(_ context.Context, prefix, 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), + "not implemented: record certificate to prefix %s with query %s for rowID %d (%d bytes)", + prefix, query, rowID, len(certBytes), )) } @@ -778,7 +843,7 @@ func (d *LegacyDB) recordSignedOpenPGPKey(ctx context.Context, rowID int, signat gpgExpiry := entity.Subkeys[0].Sig.CreationTime.Add(time.Second * time.Duration(*lifeTimeSecs)) - crtFileName, err := d.writeCertificate("gpg", rowID, signatureData) + crtFileName, err := d.writeCertificate(prefixOpenPGP, rowID, signatureData) if err != nil { return fmt.Errorf("could not write OpenPGP result: %w", err) } @@ -847,13 +912,13 @@ func (d *LegacyDB) handleSignedCertificate( switch pending.responseType { case respPersonalClientCertificate, respPersonalCodeSigningCertificate: - err = d.recordCertificate(ctx, sqlRecordPersonalClientCert, pending.rowID, r.CertificateData) + err = d.recordCertificate(ctx, prefixPersonalClient, sqlRecordPersonalClientCert, pending.rowID, r.CertificateData) case respPersonalServerCertificate: - err = d.recordCertificate(ctx, sqlRecordPersonalServerCert, pending.rowID, r.CertificateData) + err = d.recordCertificate(ctx, prefixPersonalServer, sqlRecordPersonalServerCert, pending.rowID, r.CertificateData) case respOrganizationalClientCertificate, respOrganizationalCodeSigningCertificate: - err = d.recordCertificate(ctx, sqlRecordOrganizationalClientCert, pending.rowID, r.CertificateData) + err = d.recordCertificate(ctx, prefixOrganizationalClient, sqlRecordOrganizationalClientCert, pending.rowID, r.CertificateData) case respOrganizationalServerCertificate: - err = d.recordCertificate(ctx, sqlRecordOrganizationalServerCert, pending.rowID, r.CertificateData) + err = d.recordCertificate(ctx, prefixOrganizationalServer, sqlRecordOrganizationalServerCert, pending.rowID, r.CertificateData) default: return fmt.Errorf("unexpected response type for pending request %s", announce.ID) } @@ -936,7 +1001,8 @@ func (d *LegacyDB) sendNotificationEmail(ctx context.Context, e emailData) error return fmt.Errorf("could not set text template for email body: %w", err) } - c, err := mail.NewClient("localhost") + const smtpPort = 1025 + c, err := mail.NewClient("localhost", mail.WithPort(smtpPort)) if err != nil { return fmt.Errorf("could not create mail client: %w", err) }