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
This commit is contained in:
Jan Dittberner 2024-01-12 20:48:13 +01:00
parent a6317c82c5
commit 73aad9d74e
2 changed files with 111 additions and 31 deletions

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2022-2023 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");
@ -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) 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 response.Response = &resp
case messages.RespError: case messages.RespError:
var resp messages.ErrorResponse var resp messages.ErrorResponse

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. 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. // schema, reading CSR files from the filesystem and storing certificates in the file system.
package legacydb package legacydb
@ -26,6 +26,7 @@ import (
"crypto/rand" "crypto/rand"
"crypto/x509" "crypto/x509"
"database/sql" "database/sql"
"encoding/pem"
"errors" "errors"
"fmt" "fmt"
"math/big" "math/big"
@ -172,6 +173,14 @@ SET warning = warning + 1
WHERE id = ?` WHERE id = ?`
) )
const (
prefixOpenPGP = "gpg"
prefixPersonalClient = "client"
prefixPersonalServer = "server"
prefixOrganizationalClient = "orgclient"
prefixOrganizationalServer = "orgserver"
)
type pendingResponse struct { type pendingResponse struct {
responseType responseType responseType responseType
rowID int rowID int
@ -315,6 +324,12 @@ func New(logger *logrus.Logger, config *config.Database, commands chan *protocol
respOrganizationalServerCertificate: "org_server", respOrganizationalServerCertificate: "org_server",
} }
supportedHashAlgorithms := map[string]crypto.Hash{
"sha256": crypto.SHA256,
"sha384": crypto.SHA384,
"sha512": crypto.SHA512,
}
failureQuery := map[responseType]string{ failureQuery := map[responseType]string{
respGPG: sqlRecordFailedOpenPGP, respGPG: sqlRecordFailedOpenPGP,
respPersonalClientCertificate: sqlRecordFailedPersonalClientCertificate, respPersonalClientCertificate: sqlRecordFailedPersonalClientCertificate,
@ -332,6 +347,7 @@ func New(logger *logrus.Logger, config *config.Database, commands chan *protocol
logger: logger, logger: logger,
commands: commands, commands: commands,
emails: emails, emails: emails,
algorithms: supportedHashAlgorithms,
issuerIDs: issuerIDs, issuerIDs: issuerIDs,
profiles: profiles, profiles: profiles,
pending: pending, pending: pending,
@ -540,7 +556,7 @@ func (d *LegacyDB) requestCerts(ctx context.Context, query string, rt responseTy
var ( var (
csrID int csrID int
csrFileName string csrFileName string
certType int certType sql.NullInt16
md string md string
subject 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) return fmt.Errorf("could not scan row: %w", err)
} }
csrBytes, err := os.ReadFile(csrFileName) pemBytes, err := os.ReadFile(csrFileName)
if err != nil { if err != nil {
d.logger.WithFields(logrus.Fields{ d.logger.WithFields(logrus.Fields{
"id": csrID, "id": csrID,
@ -561,6 +577,23 @@ func (d *LegacyDB) requestCerts(ctx context.Context, query string, rt responseTy
continue 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] hashAlg, ok := d.algorithms[md]
if !ok { if !ok {
d.logger.WithFields(logrus.Fields{}).Warn("unsupported hash algorithm") 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 continue
} }
signCertCommand := buildSignCertificateCommand(issuerID, profileID, csrBlock.Bytes, subjParts, hashAlg)
command := &protocol.Command{ command := &protocol.Command{
Announce: messages.BuildCommandAnnounce(messages.CmdSignCertificate), Announce: messages.BuildCommandAnnounce(messages.CmdSignCertificate),
Command: messages.SignCertificateCommand{ Command: signCertCommand,
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.Lock()
@ -617,6 +639,45 @@ func (d *LegacyDB) requestCerts(ctx context.Context, query string, rt responseTy
return nil 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 ( var (
errInvalidSANPart = errors.New("invalid SAN part, missing colon") errInvalidSANPart = errors.New("invalid SAN part, missing colon")
errUnsupportedSubjectPart = errors.New("unsupported subject part") 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 { func (d *LegacyDB) revokePersonalClientCerts(_ context.Context) error {
panic("not implemented") logrus.Debug("not implemented")
return nil
} }
func (d *LegacyDB) revokePersonalServerCerts(_ context.Context) error { func (d *LegacyDB) revokePersonalServerCerts(_ context.Context) error {
panic("not implemented") logrus.Debug("not implemented")
return nil
} }
func (d *LegacyDB) revokeOrganizationClientCerts(_ context.Context) error { func (d *LegacyDB) revokeOrganizationClientCerts(_ context.Context) error {
panic("not implemented") logrus.Debug("not implemented")
return nil
} }
func (d *LegacyDB) revokeOrganizationServerCerts(_ context.Context) error { 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) { 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 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( panic(fmt.Sprintf(
"not implemented: record certificate with query %s for rowID %d (%d bytes)", "not implemented: record certificate to prefix %s with query %s for rowID %d (%d bytes)",
query, rowID, len(certBytes), 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)) 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 { if err != nil {
return fmt.Errorf("could not write OpenPGP result: %w", err) return fmt.Errorf("could not write OpenPGP result: %w", err)
} }
@ -847,13 +912,13 @@ func (d *LegacyDB) handleSignedCertificate(
switch pending.responseType { switch pending.responseType {
case respPersonalClientCertificate, respPersonalCodeSigningCertificate: case respPersonalClientCertificate, respPersonalCodeSigningCertificate:
err = d.recordCertificate(ctx, sqlRecordPersonalClientCert, pending.rowID, r.CertificateData) err = d.recordCertificate(ctx, prefixPersonalClient, sqlRecordPersonalClientCert, pending.rowID, r.CertificateData)
case respPersonalServerCertificate: case respPersonalServerCertificate:
err = d.recordCertificate(ctx, sqlRecordPersonalServerCert, pending.rowID, r.CertificateData) err = d.recordCertificate(ctx, prefixPersonalServer, sqlRecordPersonalServerCert, pending.rowID, r.CertificateData)
case respOrganizationalClientCertificate, respOrganizationalCodeSigningCertificate: case respOrganizationalClientCertificate, respOrganizationalCodeSigningCertificate:
err = d.recordCertificate(ctx, sqlRecordOrganizationalClientCert, pending.rowID, r.CertificateData) err = d.recordCertificate(ctx, prefixOrganizationalClient, sqlRecordOrganizationalClientCert, pending.rowID, r.CertificateData)
case respOrganizationalServerCertificate: case respOrganizationalServerCertificate:
err = d.recordCertificate(ctx, sqlRecordOrganizationalServerCert, pending.rowID, r.CertificateData) err = d.recordCertificate(ctx, prefixOrganizationalServer, sqlRecordOrganizationalServerCert, pending.rowID, r.CertificateData)
default: default:
return fmt.Errorf("unexpected response type for pending request %s", announce.ID) 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) 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 { if err != nil {
return fmt.Errorf("could not create mail client: %w", err) return fmt.Errorf("could not create mail client: %w", err)
} }