Implement sign certificate command

- decouple config and messages
- cainfo maps from config.Profile to messages.CAProfile
- config parses profile usage
- validity can be configured per certificate profile, defaults are defined in
  a defaultValidity method of the profile usage
- the client simulator emits certificate signing requests at random intervals
- add implementation of SingCertificateCommand to MsgPackHandler
- remove indirection signing.RequestSignature
main
Jan Dittberner 1 year ago
parent 9c17a6730f
commit ad6b987c91

@ -21,10 +21,15 @@ package main
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"io"
mathRand "math/rand"
"os"
"sort"
"time"
@ -52,9 +57,11 @@ func (g *TestCommandGenerator) GenerateCommands(ctx context.Context) error {
}
const (
healthInterval = 5 * time.Second
crlInterval = 15 * time.Minute
startPause = 3 * time.Second
healthInterval = 5 * time.Second
crlInterval = 15 * time.Minute
startPause = 3 * time.Second
minSignInterval = 5 * time.Second
maxSignInterval = 10 * time.Second
)
g.logger.Info("start generating commands")
@ -63,11 +70,12 @@ func (g *TestCommandGenerator) GenerateCommands(ctx context.Context) error {
g.commands <- &protocol.Command{
Announce: messages.BuildCommandAnnounce(messages.CmdFetchCRL),
Command: &messages.FetchCRLCommand{IssuerID: "sub-ecc_person_2022"},
Command: &messages.FetchCRLCommand{IssuerID: "ecc_person_2022"},
}
healthTimer := time.NewTimer(healthInterval)
crlTimer := time.NewTimer(crlInterval)
signTimer := time.NewTimer(minSignInterval)
for {
select {
@ -91,12 +99,48 @@ func (g *TestCommandGenerator) GenerateCommands(ctx context.Context) error {
case <-crlTimer.C:
g.commands <- &protocol.Command{
Announce: messages.BuildCommandAnnounce(messages.CmdFetchCRL),
Command: &messages.FetchCRLCommand{IssuerID: "sub-ecc_person_2022"},
Command: &messages.FetchCRLCommand{IssuerID: "ecc_person_2022"},
}
case <-signTimer.C:
g.commands <- &protocol.Command{
Announce: messages.BuildCommandAnnounce(messages.CmdSignCertificate),
Command: &messages.SignCertificateCommand{
IssuerID: "ecc_person_2022",
ProfileName: "person",
CSRData: g.generateCsr("Test Person"),
CommonName: "Test Person",
EmailAddresses: []string{"test@example.org"},
PreferredHash: crypto.SHA256,
},
}
newRandomDuration := minSignInterval + time.Duration(mathRand.Int63n(int64(maxSignInterval)))
signTimer.Reset(newRandomDuration)
}
}
}
func (g *TestCommandGenerator) generateCsr(cn string) []byte {
keyPair, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
g.logger.WithError(err).Panic("could not generate key pair")
}
template := &x509.CertificateRequest{
SignatureAlgorithm: x509.ECDSAWithSHA256,
PublicKey: keyPair.Public(),
Subject: pkix.Name{CommonName: cn},
}
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, template, keyPair)
if err != nil {
g.logger.WithError(err).Panic("could not create signing request")
}
return csrBytes
}
type clientSimulator struct {
clientHandler protocol.ClientHandler
framesIn chan []byte

@ -35,6 +35,7 @@ import (
"git.cacert.org/cacert-gosigner/internal/hsm"
"git.cacert.org/cacert-gosigner/internal/serial"
"git.cacert.org/cacert-gosigner/internal/x509/revoking"
"git.cacert.org/cacert-gosigner/internal/x509/signing"
)
var (
@ -94,18 +95,21 @@ func main() {
healthHandler := health.New(version, access)
revokingRepositories, err := configureRepositories(caConfig, logger)
revokingRepositories, signingRepositories, err := configureRepositories(caConfig, logger)
if err != nil {
logger.WithError(err).Fatal("could not setup revoking repositories")
logger.WithError(err).Fatal("could not setup repositories")
}
fetchCRLHandler := revoking.NewFetchCRLHandler(revokingRepositories)
signX509Handler := signing.NewSignCertificateHandler(signingRepositories)
proto, err := handler.New(
logger,
handler.RegisterHealthHandler(healthHandler),
handler.RegisterFetchCRLHandler(fetchCRLHandler),
handler.RegisterCAInfoHandler(access),
handler.RegisterCertificateSigningHandler(signX509Handler),
)
if err != nil {
logger.WithError(err).Fatal("could not setup protocol handler")
@ -151,26 +155,32 @@ func runSigner(logger *logrus.Logger, serialHandler *serial.Handler) error {
func configureRepositories(
caConfig *config.SignerConfig,
logger *logrus.Logger,
) (map[string]*revoking.X509Revoking, error) {
) (map[string]*revoking.X509Revoking, map[string]map[string]*signing.X509Signing, error) {
var err error
result := make(map[string]*revoking.X509Revoking)
revokers := make(map[string]*revoking.X509Revoking)
signers := make(map[string]map[string]*signing.X509Signing)
for _, name := range caConfig.RootCAs() {
result[name], err = buildX509Revoking(caConfig, name, logger)
revokers[name], err = buildX509Revoking(caConfig, name, logger)
if err != nil {
return nil, err
return nil, nil, err
}
}
for _, name := range caConfig.SubordinateCAs() {
result[name], err = buildX509Revoking(caConfig, name, logger)
revokers[name], err = buildX509Revoking(caConfig, name, logger)
if err != nil {
return nil, nil, err
}
signers[name], err = buildX509Signing(caConfig, name, logger)
if err != nil {
return nil, err
return nil, nil, err
}
}
return result, nil
return revokers, signers, nil
}
func buildX509Revoking(
@ -197,6 +207,33 @@ func buildX509Revoking(
), nil
}
func buildX509Signing(
caConfig *config.SignerConfig,
name string,
logger *logrus.Logger,
) (map[string]*signing.X509Signing, error) {
caDef, err := caConfig.GetCADefinition(name)
if err != nil {
return nil, fmt.Errorf("could not get CA definition for %s: %w", name, err)
}
repo, err := caConfig.Repository(name)
if err != nil {
return nil, fmt.Errorf("could not get repository for %s: %w", name, err)
}
result := make(map[string]*signing.X509Signing)
for _, prof := range caDef.Profiles {
result[prof.Name] = signing.NewX509Signing(signing.NewProfile(
logger, caDef.KeyPair, caDef.Certificate, prof.Name,
[]string{caConfig.BuildOCSPURL(caDef)}, prof.UseFor, prof.Years, prof.Months, prof.Days,
), repo)
}
return result, nil
}
func initializeHSM(caConfig *config.SignerConfig, setupMode, verbose bool, logger *logrus.Logger) *hsm.Access {
opts := make([]hsm.ConfigOption, 0)

@ -17,7 +17,10 @@ limitations under the License.
package cainfo
import "git.cacert.org/cacert-gosigner/pkg/messages"
import (
"git.cacert.org/cacert-gosigner/internal/config"
"git.cacert.org/cacert-gosigner/pkg/messages"
)
type Result struct {
Certificate []byte
@ -27,3 +30,17 @@ type Result struct {
type Handler interface {
GetCAInfo(string) (*Result, error)
}
func MapProfiles(profiles []config.Profile) []messages.CAProfile {
value := make([]messages.CAProfile, len(profiles))
for i, p := range profiles {
value[i] = messages.CAProfile{
Name: p.Name,
Description: p.UseFor.Description(),
UseFor: p.UseFor,
}
}
return value
}

@ -31,8 +31,6 @@ import (
"gopkg.in/yaml.v3"
"git.cacert.org/cacert-gosigner/pkg/messages"
"git.cacert.org/cacert-gosigner/internal/x509/openssl"
"git.cacert.org/cacert-gosigner/internal/x509/revoking"
"git.cacert.org/cacert-gosigner/internal/x509/signing"
@ -533,6 +531,40 @@ func nameToCurve(name string) (elliptic.Curve, error) {
}
}
var validProfileUsages = map[string]signing.ProfileUsage{
"ocsp": signing.UsageOCSP,
"client": signing.UsageClient,
"code": signing.UsageCode,
"person": signing.UsagePerson,
"server": signing.UsageServer,
"server_client": signing.UsageServerClient,
"org_client": signing.UsageOrgClient,
"org_code": signing.UsageOrgCode,
"org_email": signing.UsageOrgEmail,
"org_person": signing.UsageOrgPerson,
"org_server": signing.UsageOrgServer,
"org_server_client": signing.UsageOrgServerClient,
}
func ParseUsage(u string) (signing.ProfileUsage, error) {
usage, ok := validProfileUsages[u]
if !ok {
return signing.UsageInvalid, fmt.Errorf("unsupported profile usage: %s", u)
}
return usage, nil
}
type Profile struct {
Name string
UseFor signing.ProfileUsage
Years int
Months int
Days int
}
type CaCertificateEntry struct {
KeyInfo *PrivateKeyInfo
CommonName string
@ -542,7 +574,7 @@ type CaCertificateEntry struct {
KeyPair crypto.Signer
Parent string
Storage string
Profiles []messages.CAProfile
Profiles []Profile
}
func (c *CaCertificateEntry) UnmarshalYAML(value *yaml.Node) error {
@ -555,8 +587,13 @@ func (c *CaCertificateEntry) UnmarshalYAML(value *yaml.Node) error {
Parent string `yaml:"parent"`
Storage string `yaml:"storage"`
Profiles []struct {
Name string `yaml:"name"`
UseFor string `yaml:"use-for"`
Name string `yaml:"name"`
UseFor string `yaml:"use-for"`
Validity struct {
Years int `yaml:"years"`
Months int `yaml:"months"`
Days int `yaml:"days"`
} `yaml:"validity"`
} `yaml:"profiles"`
}
@ -598,17 +635,24 @@ func (c *CaCertificateEntry) UnmarshalYAML(value *yaml.Node) error {
}
if m.Profiles != nil {
c.Profiles = make([]messages.CAProfile, len(m.Profiles))
c.Profiles = make([]Profile, len(m.Profiles))
for i, prof := range m.Profiles {
usage, err := messages.ParseUsage(prof.UseFor)
usage, err := ParseUsage(prof.UseFor)
if err != nil {
return fmt.Errorf("config error: %w", err)
}
c.Profiles[i] = messages.CAProfile{
if prof.Validity.Years > 0 || prof.Validity.Months > 0 || prof.Validity.Days > 0 {
prof.Validity.Years, prof.Validity.Months, prof.Validity.Days = defaultValidity(usage)
}
c.Profiles[i] = Profile{
Name: prof.Name,
UseFor: usage,
Years: prof.Validity.Years,
Months: prof.Validity.Months,
Days: prof.Validity.Days,
}
}
}
@ -616,6 +660,15 @@ func (c *CaCertificateEntry) UnmarshalYAML(value *yaml.Node) error {
return nil
}
func defaultValidity(useFor signing.ProfileUsage) (years int, months int, days int) {
switch useFor {
case signing.UsagePerson, signing.UsageOrgPerson, signing.UsageOrgEmail:
return 2, 0, 0
default:
return 1, 0, 0
}
}
func (c *CaCertificateEntry) IsRoot() bool {
return c.Parent == ""
}

@ -31,6 +31,7 @@ import (
"gopkg.in/yaml.v3"
"git.cacert.org/cacert-gosigner/internal/config"
"git.cacert.org/cacert-gosigner/internal/x509/signing"
)
type TestCurve struct {
@ -992,3 +993,26 @@ func TestSignerConfig_GetKeyStorage(t *testing.T) {
assert.ErrorContains(t, err, "could not find storage definition with label")
assert.Nil(t, keyStorage)
}
func TestParseUsage(t *testing.T) {
okValues := []string{
"ocsp", "client", "code", "person", "server", "server_client",
"org_client", "org_code", "org_email", "org_person", "org_server", "org_server_client",
}
for _, v := range okValues {
t.Run(v, func(t *testing.T) {
u, err := config.ParseUsage(v)
assert.NoError(t, err)
assert.Greater(t, u, signing.ProfileUsage(0))
})
}
t.Run("invalid", func(t *testing.T) {
u, err := config.ParseUsage("foo")
assert.Error(t, err)
assert.Equal(t, u, signing.UsageInvalid)
})
}

@ -19,6 +19,8 @@ package handler
import (
"context"
"crypto/x509"
"crypto/x509/pkix"
"errors"
"fmt"
"math/big"
@ -30,6 +32,7 @@ import (
"git.cacert.org/cacert-gosigner/internal/cainfo"
"git.cacert.org/cacert-gosigner/internal/health"
"git.cacert.org/cacert-gosigner/internal/x509/revoking"
"git.cacert.org/cacert-gosigner/internal/x509/signing"
"git.cacert.org/cacert-gosigner/pkg/messages"
"git.cacert.org/cacert-gosigner/pkg/protocol"
)
@ -44,6 +47,7 @@ type MsgPackHandler struct {
healthHandler *health.Handler
certificateAuthorityInfoHandler cainfo.Handler
fetchCRLHandler *revoking.FetchCRLHandler
x509SigningHandler signing.Handler
}
func (m *MsgPackHandler) CommandAnnounce(ctx context.Context, frames <-chan []byte) (*protocol.Command, error) {
@ -174,6 +178,18 @@ func (m *MsgPackHandler) parseFetchCRLCommand(frame []byte) (*messages.FetchCRLC
return &command, nil
}
func (m *MsgPackHandler) parseSignCertificateCommand(frame []byte) (*messages.SignCertificateCommand, error) {
var command messages.SignCertificateCommand
if err := msgpack.Unmarshal(frame, &command); err != nil {
m.logger.WithError(err).Error("unmarshal failed")
return nil, errors.New("could not unmarshal sign certificate command")
}
return &command, nil
}
func (m *MsgPackHandler) handleCommand(command *protocol.Command) (*protocol.Response, error) {
var (
responseCode messages.ResponseCode
@ -202,6 +218,13 @@ func (m *MsgPackHandler) handleCommand(command *protocol.Command) (*protocol.Res
}
responseCode, responseData = messages.RespFetchCRL, response
case *messages.SignCertificateCommand:
response, err := m.handleSignCertificateCommand(cmd)
if err != nil {
return nil, err
}
responseCode, responseData = messages.RespSignCertificate, response
default:
return nil, fmt.Errorf("unhandled command %s", command.Announce)
}
@ -242,6 +265,13 @@ func (m *MsgPackHandler) parseCommand(frame []byte, command *protocol.Command) e
}
command.Command = fetchCRLCommand
case messages.CmdSignCertificate:
signCertificateCommand, err := m.parseSignCertificateCommand(frame)
if err != nil {
return err
}
command.Command = signCertificateCommand
default:
return fmt.Errorf("unhandled command code %s", command.Announce.Code)
}
@ -315,6 +345,43 @@ func (m *MsgPackHandler) handleFetchCRLCommand(command *messages.FetchCRLCommand
return response, nil
}
func (m *MsgPackHandler) handleSignCertificateCommand(
command *messages.SignCertificateCommand,
) (*messages.SignCertificateResponse, error) {
csr, err := x509.ParseCertificateRequest(command.CSRData)
if err != nil {
return nil, fmt.Errorf("could not parse certificate signing request: %w", err)
}
signerRequest := &signing.SignerRequest{
CSR: csr,
SubjectDN: pkix.Name{CommonName: command.CommonName},
Emails: command.EmailAddresses,
DNSNames: command.Hostnames,
PreferredHash: command.PreferredHash,
}
if command.Organization != "" {
signerRequest.SubjectDN.Organization = []string{command.Organization}
}
if command.OrganizationalUnit != "" {
signerRequest.SubjectDN.OrganizationalUnit = []string{command.OrganizationalUnit}
}
x509Signing, err := m.x509SigningHandler.GetSigner(command.IssuerID, command.ProfileName)
if err != nil {
return nil, fmt.Errorf("could not get X.509 signing component: %w", err)
}
res, err := x509Signing.Sign(signerRequest)
if err != nil {
return nil, fmt.Errorf("could not sign certificate: %w", err)
}
return &messages.SignCertificateResponse{CertificateData: res.Certificate.Raw}, nil
}
func New(logger *logrus.Logger, handlers ...RegisterHandler) (protocol.ServerHandler, error) {
messages.RegisterGeneratedResolver()
@ -348,3 +415,9 @@ func RegisterCAInfoHandler(caInfoHandler cainfo.Handler) func(handler *MsgPackHa
h.certificateAuthorityInfoHandler = caInfoHandler
}
}
func RegisterCertificateSigningHandler(signingHandler signing.Handler) func(handler *MsgPackHandler) {
return func(h *MsgPackHandler) {
h.x509SigningHandler = signingHandler
}
}

@ -194,7 +194,7 @@ func (a *Access) GetCAInfo(name string) (*cainfo.Result, error) {
return &cainfo.Result{
Certificate: certificate.Raw,
Profiles: def.Profiles,
Profiles: cainfo.MapProfiles(def.Profiles),
}, nil
}

@ -19,35 +19,17 @@ limitations under the License.
package signing
import (
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"time"
)
type SignerRequest struct {
CSR *x509.CertificateRequest
SubjectDN pkix.Name
Emails []string
DNSNames []string
Duration time.Duration
SignatureAlgorithm x509.SignatureAlgorithm
}
func NewSignerRequest(
csr *x509.CertificateRequest,
subjectDN pkix.Name,
emails, dnsNames []string,
duration time.Duration,
signatureAlgorithm x509.SignatureAlgorithm,
) *SignerRequest {
return &SignerRequest{
CSR: csr,
SubjectDN: subjectDN,
Emails: emails,
DNSNames: dnsNames,
Duration: duration,
SignatureAlgorithm: signatureAlgorithm,
}
CSR *x509.CertificateRequest
SubjectDN pkix.Name
Emails []string
DNSNames []string
PreferredHash crypto.Hash
}
type SignerResponse struct {

@ -18,12 +18,23 @@ limitations under the License.
package signing
import (
"crypto"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"time"
"github.com/sirupsen/logrus"
"git.cacert.org/cacert-gosigner/internal/x509/helper"
)
// PolicyIdentifierCAcertClass3Policy is the ASN.1 object identifier for the CAcert Class3 policy from [OIDAllocation]
//
// [OIDAllocation]: https://wiki.cacert.org/OidAllocation
var PolicyIdentifierCAcertClass3Policy = asn1.ObjectIdentifier{1, 3, 6, 4, 1, 18506, 2, 3}
type X509Signing struct {
signer Signer
repo Repository
@ -33,32 +44,6 @@ func NewX509Signing(signer Signer, repo Repository) *X509Signing {
return &X509Signing{signer: signer, repo: repo}
}
type RequestSignature struct {
rawCSRData []byte
subjectCommonName string
emails []string
dnsNames []string
duration time.Duration
signatureAlgorithm x509.SignatureAlgorithm
}
func NewRequestSignature(
csrBytes []byte,
cn string,
emails, dnsNames []string,
duration time.Duration,
signatureAlgorithm x509.SignatureAlgorithm,
) *RequestSignature {
return &RequestSignature{
rawCSRData: csrBytes,
subjectCommonName: cn,
emails: emails,
dnsNames: dnsNames,
duration: duration,
signatureAlgorithm: signatureAlgorithm,
}
}
type CertificateSigned struct {
certificate *x509.Certificate
}
@ -67,23 +52,8 @@ func (c CertificateSigned) Certificate() *x509.Certificate {
return c.certificate
}
func (x *X509Signing) Sign(signingRequest *RequestSignature) (*CertificateSigned, error) {
// validate request content
csr, err := x509.ParseCertificateRequest(signingRequest.rawCSRData)
if err != nil {
return nil, fmt.Errorf("could not parse CSR data: %w", err)
}
certificateFromSigner, err := x.signer.SignCertificate(
NewSignerRequest(
csr,
pkix.Name{CommonName: signingRequest.subjectCommonName},
signingRequest.emails,
signingRequest.dnsNames,
signingRequest.duration,
signingRequest.signatureAlgorithm,
),
)
func (x *X509Signing) Sign(signingRequest *SignerRequest) (*SignerResponse, error) {
certificateFromSigner, err := x.signer.SignCertificate(signingRequest)
if err != nil {
return nil, fmt.Errorf("could not sign certificate: %w", err)
}
@ -93,5 +63,255 @@ func (x *X509Signing) Sign(signingRequest *RequestSignature) (*CertificateSigned
return nil, fmt.Errorf("could not store certificate: %w", err)
}
return &CertificateSigned{certificate: certificateFromSigner.Certificate}, nil
return certificateFromSigner, nil
}
type Handler interface {
GetSigner(id string, profile string) (*X509Signing, error)
}
type ProfileUsage uint8
const (
UsageInvalid ProfileUsage = iota
UsageOCSP
UsageClient
UsageCode
UsagePerson
UsageServer
UsageServerClient
UsageOrgClient
UsageOrgCode
UsageOrgEmail
UsageOrgPerson
UsageOrgServer
UsageOrgServerClient
)
var profileUsageNames = map[ProfileUsage]string{
UsageInvalid: "invalid",
UsageOCSP: "ocsp",
UsageClient: "client",
UsageCode: "code",
UsagePerson: "person",
UsageServer: "server",
UsageServerClient: "server_client",
UsageOrgClient: "org_client",
UsageOrgCode: "org_code",
UsageOrgEmail: "org_email",
UsageOrgPerson: "org_person",
UsageOrgServer: "org_server",
UsageOrgServerClient: "org_server_client",
}
func (p ProfileUsage) String() string {
name, ok := profileUsageNames[p]
if !ok {
return fmt.Sprintf("unknown profile usage %d", p)
}
return name
}
var profileUsageDescriptions = map[ProfileUsage]string{
UsageInvalid: "Invalid certificate profile, not to be used",
UsageOCSP: "OCSP responder signing certificate",
UsageClient: "machine TLS client certificate",
UsageCode: "individual code signing certificate",
UsagePerson: "person identity certificate",
UsageServer: "TLS server certificate",
UsageServerClient: "combined TLS server and client certificate",
UsageOrgClient: "organization machine TLS client certificate",
UsageOrgCode: "organization code signing certificate",
UsageOrgEmail: "organization email certificate",
UsageOrgPerson: "organizational person identity certificate",
UsageOrgServer: "organization TLS server certificate",
UsageOrgServerClient: "combined organization TLS server and client certificate",
}
func (p ProfileUsage) Description() string {
description, ok := profileUsageDescriptions[p]
if !ok {
return fmt.Sprintf("unknown profile usage %d", p)
}
return description
}
type Profile struct {
logger *logrus.Logger
privateKey crypto.Signer
certificate *x509.Certificate
ocspURLs []string
name string
usage ProfileUsage
years int
months int
days int
}
func (p *Profile) SignCertificate(request *SignerRequest) (*SignerResponse, error) {
signatureAlgorithm, err := p.determineSignatureAlgorithm(request.PreferredHash)
if err != nil {
return nil, err
}
notBefore := time.Now().UTC()
serialNumber, err := helper.GenerateRandomSerial()
if err != nil {
return nil, fmt.Errorf("could not generate certificate serial number: %w", err)
}
const x509v3 = 3
template := &x509.Certificate{
SignatureAlgorithm: signatureAlgorithm,
Version: x509v3,
SerialNumber: serialNumber,
Issuer: p.certificate.Subject,
Subject: request.SubjectDN,
NotBefore: notBefore,
NotAfter: p.setNotAfter(notBefore),
KeyUsage: p.keyUsage(),
ExtKeyUsage: p.extKeyUsage(),
IsCA: false,
OCSPServer: p.ocspURLs,
DNSNames: request.DNSNames,
EmailAddresses: request.Emails,
CRLDistributionPoints: p.certificate.CRLDistributionPoints,
PolicyIdentifiers: p.policyIdentifiers(),
}
certBytes, err := x509.CreateCertificate(rand.Reader, template, p.certificate, request.CSR.PublicKey, p.privateKey)
if err != nil {
return nil, fmt.Errorf("certificate signing failed: %w", err)
}
signedCertificate, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, fmt.Errorf("failed to build certificate from DER data: %w", err)
}
return &SignerResponse{Certificate: signedCertificate}, nil
}
func (p *Profile) setNotAfter(before time.Time) time.Time {
return before.AddDate(p.years, p.months, p.days)
}
func (p *Profile) determineSignatureAlgorithm(hash crypto.Hash) (x509.SignatureAlgorithm, error) {
switch p.certificate.PublicKeyAlgorithm {
case x509.RSA:
switch hash {
case crypto.SHA512:
return x509.SHA512WithRSA, nil
case crypto.SHA384:
return x509.SHA384WithRSA, nil
default:
return x509.SHA256WithRSA, nil
}
case x509.ECDSA:
switch hash {
case crypto.SHA512:
return x509.ECDSAWithSHA512, nil
case crypto.SHA384:
return x509.ECDSAWithSHA384, nil
default:
return x509.ECDSAWithSHA384, nil
}
default:
return x509.UnknownSignatureAlgorithm, fmt.Errorf(
"could not determine signature algorithm for public key algorithm %s",
p.certificate.PublicKeyAlgorithm,
)
}
}
func (p *Profile) keyUsage() x509.KeyUsage {
switch p.usage {
case UsageClient, UsagePerson, UsageServer, UsageServerClient, UsageOrgClient, UsageOrgEmail, UsageOrgPerson,
UsageOrgServer, UsageOrgServerClient:
return x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
default:
return x509.KeyUsageDigitalSignature
}
}
func (p *Profile) extKeyUsage() []x509.ExtKeyUsage {
switch p.usage {
case UsageClient, UsageOrgClient:
return []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
case UsagePerson, UsageOrgPerson:
return []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageEmailProtection}
case UsageOrgEmail:
return []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection}
case UsageCode, UsageOrgCode:
return []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}
case UsageServer, UsageOrgServer:
return []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
case UsageServerClient, UsageOrgServerClient:
return []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}
case UsageOCSP:
return []x509.ExtKeyUsage{x509.ExtKeyUsageOCSPSigning}
default:
return nil
}
}
func (p *Profile) policyIdentifiers() []asn1.ObjectIdentifier {
switch p.usage {
case UsageOCSP:
return nil
default:
return []asn1.ObjectIdentifier{PolicyIdentifierCAcertClass3Policy}
}
}
func NewProfile(
logger *logrus.Logger, privateKey crypto.Signer, certificate *x509.Certificate, name string, ocspURLs []string,
useFor ProfileUsage, years int, months int, days int,
) *Profile {
return &Profile{
privateKey: privateKey,
certificate: certificate,
ocspURLs: ocspURLs,
name: name,
usage: useFor,
years: years,
months: months,
days: days,
logger: logger,
}
}
type SignCertificateHandler struct {
profiles map[string]map[string]*X509Signing
}
func (s *SignCertificateHandler) GetSigner(issuerID string, profileName string) (*X509Signing, error) {
profiles, ok := s.profiles[issuerID]
if !ok {
return nil, fmt.Errorf("no CA definition found for issuer id %s", issuerID)
}
profile, ok := profiles[profileName]
if !ok {
return nil, fmt.Errorf("unknown profile %s for issuer %s", profileName, issuerID)
}
return profile, nil
}
func NewSignCertificateHandler(profiles map[string]map[string]*X509Signing) *SignCertificateHandler {
return &SignCertificateHandler{profiles: profiles}
}

@ -19,6 +19,8 @@ package signing_test
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
@ -45,6 +47,83 @@ func randomSerial(t *testing.T) *big.Int {
return serial
}
func TestProfileUsage_String(t *testing.T) {
okValues := []struct {
Name string
Usage signing.ProfileUsage
}{
{"invalid", signing.UsageInvalid},
{"ocsp", signing.UsageOCSP},
{"client", signing.UsageClient},
{"code", signing.UsageCode},
{"person", signing.UsagePerson},
{"server", signing.UsageServer},
{"server-client", signing.UsageServerClient},
{"org-client", signing.UsageOrgClient},
{"org-code", signing.UsageOrgCode},
{"org-email", signing.UsageOrgEmail},
{"org-person", signing.UsageOrgPerson},
{"org-server", signing.UsageOrgServer},
{"org-server-client", signing.UsageOrgServerClient},
}
for _, v := range okValues {
t.Run(v.Name, func(t *testing.T) {
str := v.Usage.String()
assert.NotEmpty(t, str)
assert.NotContains(t, str, "unknown profile usage")
})
}
t.Run("undefined", func(t *testing.T) {
str := signing.ProfileUsage(255).String()
assert.NotEmpty(t, str)
assert.Contains(t, str, "unknown profile usage")
assert.Contains(t, str, "255")
})
}
func TestProfileUsage_Description(t *testing.T) {
okValues := []struct {
Name string
Usage signing.ProfileUsage
}{
{"invalid", signing.UsageInvalid},
{"ocsp", signing.UsageOCSP},
{"client", signing.UsageClient},
{"code", signing.UsageCode},
{"person", signing.UsagePerson},
{"server", signing.UsageServer},
{"server-client", signing.UsageServerClient},
{"org-client", signing.UsageOrgClient},
{"org-code", signing.UsageOrgCode},
{"org-email", signing.UsageOrgEmail},
{"org-person", signing.UsageOrgPerson},
{"org-server", signing.UsageOrgServer},
{"org-server-client", signing.UsageOrgServerClient},
}
for _, v := range okValues {
t.Run(v.Name, func(t *testing.T) {
str := v.Usage.Description()
assert.NotEmpty(t, str)
assert.NotContains(t, str, "unknown profile usage")
assert.Greater(t, len(str), len(v.Name))
})
}
t.Run("undefined", func(t *testing.T) {
str := signing.ProfileUsage(255).Description()
assert.NotEmpty(t, str)
assert.Contains(t, str, "unknown profile usage")
assert.Contains(t, str, "255")
})
}
type testRepo struct {
certs map[string]x509.Certificate
}
@ -72,10 +151,10 @@ func (s *testSigner) SignCertificate(request *signing.SignerRequest) (*signing.S
SerialNumber: randomSerial(s.t),
EmailAddresses: request.Emails,
NotBefore: startDate,
NotAfter: startDate.Add(request.Duration),
NotAfter: startDate.AddDate(1, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageDataEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageEmailProtection},
SignatureAlgorithm: request.SignatureAlgorithm,
SignatureAlgorithm: s.determineSignatureAlgorithm(request.PreferredHash),
}
certBytes, err := x509.CreateCertificate(rand.Reader, template, s.certificate, request.CSR.PublicKey, s.key)
@ -91,6 +170,17 @@ func (s *testSigner) SignCertificate(request *signing.SignerRequest) (*signing.S
return newTestSignerResponse(certificate), nil
}
func (s *testSigner) determineSignatureAlgorithm(hash crypto.Hash) x509.SignatureAlgorithm {
switch hash {
case crypto.SHA512:
return x509.ECDSAWithSHA512
case crypto.SHA384:
return x509.ECDSAWithSHA384
default:
return x509.ECDSAWithSHA384
}
}
func TestSigning(t *testing.T) {
testRepository := testRepo{certs: make(map[string]x509.Certificate)}
testSigner := newTestSigner(t)
@ -103,26 +193,20 @@ func TestSigning(t *testing.T) {
csrTemplate := &x509.CertificateRequest{PublicKey: csrKey.Public()}
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, csrKey)
if err != nil {
t.Error(err)
testRequest := &signing.SignerRequest{
CSR: csrTemplate,
SubjectDN: pkix.Name{CommonName: "Test Subject"},
Emails: []string{"test@example.org"},
DNSNames: nil,
PreferredHash: crypto.SHA384,
}
testRequest := signing.NewRequestSignature(
csrBytes,
"Test Subject",
[]string{"test@example.org"},
nil,
365*24*time.Hour,
x509.SHA384WithRSA,
)
signed, err := s.Sign(testRequest)
if err != nil {
t.Error(err)
}
cert := signed.Certificate()
cert := signed.Certificate
assert.Contains(t, testRepository.certs, cert.SerialNumber.Text(16))
assert.Equal(t, cert.Subject.CommonName, "Test Subject")
assert.Contains(t, cert.EmailAddresses, "test@example.org")
@ -131,7 +215,7 @@ func TestSigning(t *testing.T) {
func newTestSigner(t *testing.T) *testSigner {
t.Helper()
caKey, err := rsa.GenerateKey(rand.Reader, 3072)
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("could not generate key pair: %v", err)
}

@ -21,6 +21,7 @@ limitations under the License.
package messages
import (
"crypto"
"crypto/x509"
"encoding/pem"
"fmt"
@ -32,6 +33,8 @@ import (
// required for msgpackgen
_ "github.com/dave/jennifer"
"github.com/google/uuid"
"git.cacert.org/cacert-gosigner/internal/x509/signing"
)
type CommandCode int8
@ -126,12 +129,23 @@ func BuildResponseAnnounce(code ResponseCode, commandID string) *ResponseAnnounc
return &ResponseAnnounce{Code: code, ID: commandID, Created: time.Now().UTC()}
}
type HealthCommand struct{}
type CAProfile struct {
Name string `msgpack:"name"`
Description string `msgpack:"description"`
UseFor signing.ProfileUsage `msgpack:"use-for"`
}
func (h *HealthCommand) String() string {
return ""
func (p CAProfile) String() string {
return fmt.Sprintf("profile['%s': '%s']", p.Name, p.UseFor)
}
type CertificateStatus string
const (
CertStatusOk CertificateStatus = "ok"
CertStatusFailed CertificateStatus = "failed"
)
type CAInfoCommand struct {
Name string `msgpack:"name"`
}
@ -140,6 +154,25 @@ func (r *CAInfoCommand) String() string {
return fmt.Sprintf("name=%s", r.Name)
}
type CAInfoResponse struct {
Name string `msgpack:"name"`
Certificate []byte `msgpack:"certificate"`
Signing bool `msgpack:"signing"`
Profiles []CAProfile `msgpack:"profiles"`
}
func (i CAInfoResponse) String() string {
return fmt.Sprintf("certificate name=%s, signing=%t, profiles=[%s]", i.Name, i.Signing, i.Profiles)
}
type ErrorResponse struct {
Message string `msgpack:"message"`
}
func (e *ErrorResponse) String() string {
return fmt.Sprintf("message=%s", e.Message)
}
type FetchCRLCommand struct {
IssuerID string `msgpack:"issuer_id"`
LastKnownID []byte `msgpack:"last_known_id"`
@ -157,121 +190,64 @@ func (f *FetchCRLCommand) String() string {
return builder.String()
}
type ProfileUsage uint8
const (
UsageInvalid ProfileUsage = iota
UsageOCSP
UsageClient
UsageCode
UsagePerson
UsageServer
UsageServerClient
UsageOrgClient
UsageOrgCode
UsageOrgEmail
UsageOrgPerson
UsageOrgServer
UsageOrgServerClient
)
var validProfileUsages = map[string]ProfileUsage{
"ocsp": UsageOCSP,
"client": UsageClient,
"code": UsageCode,
"person": UsagePerson,
"server": UsageServer,
"server_client": UsageServerClient,
"org_client": UsageOrgClient,
"org_code": UsageOrgCode,
"org_email": UsageOrgEmail,
"org_person": UsageOrgPerson,
"org_server": UsageOrgServer,
"org_server_client": UsageOrgServerClient,
type FetchCRLResponse struct {
IssuerID string `msgpack:"issuer_id"`
IsDelta bool `msgpack:"is_delta"`
UnChanged bool `msgpack:"unchanged"`
CRLData []byte `msgpack:"crl_data"`
CRLNumber []byte `msgpack:"crl_number"`
}
var profileUsageDetails = map[ProfileUsage]struct {
Name string
Description string
}{
UsageInvalid: {"invalid", "Invalid certificate profile, not to be used"},
UsageOCSP: {"ocsp", "OCSP responder signing certificate"},
UsageClient: {"client", "machine TLS client certificate"},
UsageCode: {"code", "individual code signing certificate"},
UsagePerson: {"person", "person identity certificate"},
UsageServer: {"server", "TLS server certificate"},
UsageServerClient: {"server_client", "combined TLS server and client certificate"},
UsageOrgClient: {"org_client", "organization machine TLS client certificate"},
UsageOrgCode: {"org_code", "organization code signing certificate"},
UsageOrgEmail: {"org_email", "organization email certificate"},
UsageOrgPerson: {"org_person", "organizational person identity certificate"},
UsageOrgServer: {"org_server", "organization TLS server certificate"},
UsageOrgServerClient: {
"org_server_client",
"combined organization TLS server and client certificate",
},
}
func (r *FetchCRLResponse) String() string {
builder := &strings.Builder{}
func (p ProfileUsage) String() string {
name, ok := profileUsageDetails[p]
if !ok {
return fmt.Sprintf("unknown profile usage %d", p)
_, _ = fmt.Fprintf(
builder,
"issuer id=%s, delta=%t, unchanged=%t, CRL number=0x%x",
r.IssuerID,
r.IsDelta,
r.UnChanged,
new(big.Int).SetBytes(r.CRLNumber),
)
if r.UnChanged {
return builder.String()
}
return name.Name
}
if r.IsDelta {
_, _ = fmt.Fprintf(builder, ", delta CRL data of %d bytes not shown", len(r.CRLData))
func (p ProfileUsage) Description() string {
name, ok := profileUsageDetails[p]
if !ok {
return fmt.Sprintf("unknown profile usage %d", p)
return builder.String()
}
return name.Description
}
revocationList, err := x509.ParseRevocationList(r.CRLData)
if err != nil {
_, _ = fmt.Fprintf(builder, ", could not parse CRL: %s", err.Error())
func ParseUsage(u string) (ProfileUsage, error) {
usage, ok := validProfileUsages[u]
if !ok {
return UsageInvalid, fmt.Errorf("unsupported profile usage: %s", u)
return builder.String()
}
return usage, nil
}
type CAProfile struct {
Name string `msgpack:"name"`
UseFor ProfileUsage `msgpack:"use-for"`
}
_, _ = fmt.Fprintf(
builder,
", CRL info: issuer=%s, number=0x%x, next update=%s, revoked certificates=%d",
revocationList.Issuer,
revocationList.Number,
revocationList.NextUpdate,
len(revocationList.RevokedCertificates),
)
_, _ = builder.WriteString(", CRL data:\n")
_ = pem.Encode(builder, &pem.Block{
Type: "CERTIFICATE REVOCATION LIST",
Bytes: r.CRLData,
})
func (p CAProfile) String() string {
return fmt.Sprintf("profile['%s': '%s']", p.Name, p.UseFor)
return builder.String()
}
type CertificateStatus string
const (
CertStatusOk CertificateStatus = "ok"
CertStatusFailed CertificateStatus = "failed"
)
type CAInfoResponse struct {
Name string `msgpack:"name"`
Certificate []byte `msgpack:"certificate"`
Signing bool `msgpack:"signing"`
Profiles []CAProfile `msgpack:"profiles,omitempty"`
}
type HealthCommand struct{}
func (i CAInfoResponse) String() string {
return fmt.Sprintf("certificate name=%s, signing=%t, profiles=[%s]", i.Name, i.Signing, i.Profiles)
func (h *HealthCommand) String() string {
return ""
}
type HealthInfo struct {
@ -331,64 +307,56 @@ func (h *HealthResponse) String() string {
return builder.String()
}
type FetchCRLResponse struct {
IssuerID string `msgpack:"issuer_id"`
IsDelta bool `msgpack:"is_delta"`
UnChanged bool `msgpack:"unchanged"`
CRLData []byte `msgpack:"crl_data"`
CRLNumber []byte `msgpack:"crl_number"`
type SignCertificateCommand struct {
IssuerID string `msgpack:"issuer_id"`
ProfileName string `msgpack:"profile_name"`
CSRData []byte `msgpack:"csr_data"`
CommonName string `msgpack:"cn"`
Organization string `msgpack:"o"`
OrganizationalUnit string `msgpack:"ou"`
Hostnames []string `msgpack:"hostnames"`
EmailAddresses []string `msgpack:"email_addresses"`
PreferredHash crypto.Hash `msgpack:"preferred_hash"`
}
func (r *FetchCRLResponse) String() string {
func (s *SignCertificateCommand) String() string {
builder := &strings.Builder{}
_, _ = fmt.Fprintf(
builder,
"issuer id=%s, delta=%t, unchanged=%t, CRL number=0x%x",
r.IssuerID,
r.IsDelta,
r.UnChanged,
new(big.Int).SetBytes(r.CRLNumber),
builder, "issuer_id=%s, profile_name=%s, cn=%s", s.IssuerID, s.ProfileName, s.CommonName,
)
if r.UnChanged {
return builder.String()
if s.Organization != "" {
_, _ = fmt.Fprintf(builder, ", o=%s", s.Organization)
}
if r.IsDelta {
_, _ = fmt.Fprintf(builder, ", delta CRL data of %d bytes not shown", len(r.CRLData))
return builder.String()
if s.OrganizationalUnit != "" {
_, _ = fmt.Fprintf(builder, ", ou=%s", s.OrganizationalUnit)
}
revocationList, err := x509.ParseRevocationList(r.CRLData)
if err != nil {
_, _ = fmt.Fprintf(builder, ", could not parse CRL: %s", err.Error())
if len(s.Hostnames) > 0 {
builder.WriteString(", hostnames=[")
return builder.String()
builder.WriteString(strings.Join(s.Hostnames, ", "))
builder.WriteRune(']')
}
_, _ = fmt.Fprintf(
builder,
", CRL info: issuer=%s, number=0x%x, next update=%s, revoked certificates=%d",
revocationList.Issuer,
revocationList.Number,
revocationList.NextUpdate,
len(revocationList.RevokedCertificates),
)
_, _ = builder.WriteString(", CRL data:\n")
_ = pem.Encode(builder, &pem.Block{
Type: "CERTIFICATE REVOCATION LIST",
Bytes: r.CRLData,
})
if len(s.EmailAddresses) > 0 {
builder.WriteString(", email_addresses=[")
builder.WriteString(strings.Join(s.Hostnames, ", "))
builder.WriteRune(']')
}
return builder.String()
}
type ErrorResponse struct {
Message string `msgpack:"message"`
type SignCertificateResponse struct {
CertificateData []byte `msgpack:"cert_data"`
}
func (e *ErrorResponse) String() string {
return fmt.Sprintf("message=%s", e.Message)
func (r *SignCertificateResponse) String() string {
return fmt.Sprintf("cert_data of %d bytes", len(r.CertificateData))
}

@ -28,32 +28,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"git.cacert.org/cacert-gosigner/internal/x509/signing"
"git.cacert.org/cacert-gosigner/pkg/messages"
)
func TestParseUsage(t *testing.T) {
okValues := []string{
"ocsp", "client", "code", "person",