diff --git a/datastructures/signerrequest.go b/datastructures/signerrequest.go
index e477c7e..1c7a119 100644
--- a/datastructures/signerrequest.go
+++ b/datastructures/signerrequest.go
@@ -90,12 +90,19 @@ func (r *SignerRequest) String() string {
 		r.MdAlgorithm,
 		r.Days,
 		r.Spkac,
-		r.Content1,
-		r.Content2,
-		r.Content3,
+		shorten(r.Content1),
+		shorten(r.Content2),
+		shorten(r.Content3),
 	)
 }
 
+func shorten(original []byte) []byte {
+	if len(original) > 20 {
+		return original[:20]
+	}
+	return original
+}
+
 func NewNulRequest() *SignerRequest {
 	return &SignerRequest{
 		Version:  shared.ProtocolVersion,
diff --git a/go.mod b/go.mod
index c029a7c..f6f9b4d 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module git.cacert.org/cacert-gosigner
 go 1.15
 
 require (
+	github.com/longsleep/pkac v0.0.0-20191013204540-205111305195
 	github.com/sirupsen/logrus v1.7.0
 	go.bug.st/serial v1.1.1
 	golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
diff --git a/go.sum b/go.sum
index e02ce14..cfd49f5 100644
--- a/go.sum
+++ b/go.sum
@@ -5,6 +5,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/jandd/crypto v0.0.0-20210106144236-c3a8dd255ad6 h1:CfOE6Sr6BvfT6R90AgKcospJGP5+hwYhOjFR1XVb68Q=
 github.com/jandd/crypto v0.0.0-20210106144236-c3a8dd255ad6/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+github.com/longsleep/pkac v0.0.0-20191013204540-205111305195 h1:Ze//Gia3DrTxmw6IiBCusbLcSobh7dBYceVkasDg2vA=
+github.com/longsleep/pkac v0.0.0-20191013204540-205111305195/go.mod h1:Ck+2Ip7E9leckac1Bt/z0fdjmGCmR87IQsISZX7/qE0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
diff --git a/signer/command_processor.go b/signer/command_processor.go
index a8465cd..bd099e1 100644
--- a/signer/command_processor.go
+++ b/signer/command_processor.go
@@ -94,14 +94,14 @@ func (p *CommandProcessor) handleSignAction(
 	*datastructures.SignerResponse,
 	error,
 ) {
-	log.Debugf("handle sign call: %v", command)
+	log.Debugf("handle sign call: %s", command)
 
 	idSystem, err := p.checkIdentitySystem(
 		command.System, command.Root, command.Profile, command.MdAlgorithm)
 	if err != nil {
 		return nil, err
 	}
-	log.Debugf("identified id system: %+v", idSystem)
+	log.Debugf("identified id system: %s", idSystem)
 
 	switch command.System {
 	case CsX509:
@@ -167,6 +167,10 @@ type IdSystemParameters struct {
 	MessageDigestAlgorithm interface{}
 }
 
+func (s *IdSystemParameters) String() string {
+	return fmt.Sprintf("%s r:%s p:%s m:%s", s.System, s.Root, s.Profile, s.MessageDigestAlgorithm)
+}
+
 func (p *CommandProcessor) checkIdentitySystem(
 	systemId shared.CryptoSystemId,
 	rootId shared.CryptoSystemRootId,
@@ -307,7 +311,38 @@ func (p *CommandProcessor) buildXDelta(oldFile string, newFile string) ([]byte,
 }
 
 func (p *CommandProcessor) signX509Certificate(system *IdSystemParameters, days uint16, spkac uint8, request []byte, san []byte, subject []byte) ([]byte, error) {
-	return nil, errors.New("signX509Certificate is not implemented yet")
+	x509Root := system.Root.(*x509_ops.Root)
+	signatureAlgorithm := system.MessageDigestAlgorithm.(x509.SignatureAlgorithm)
+	profile := system.Profile.(*x509_ops.Profile)
+
+	log.Debugf(
+		"sign X.509 certificate for root %s using profile %s and signature algorithm %s",
+		x509Root,
+		profile,
+		signatureAlgorithm,
+	)
+	log.Debugf(
+		"client wants %d days spkac is %v, requested subject '%s' and subjectAlternativeNames '%s'",
+		days, spkac == 1, subject, san,
+	)
+	log.Tracef("the following CSR should be signed\n%s", request)
+
+	content, err := x509Root.SignCertificate(
+		profile,
+		signatureAlgorithm,
+		&x509_ops.SigningRequestParameters{
+			Request:                 request,
+			Subject:                 subject,
+			SubjectAlternativeNames: san,
+			Days:                    days,
+			IsSpkac:                 spkac == 1,
+		},
+	)
+	if err != nil {
+		return nil, fmt.Errorf("could not sign X.509 CSR with root %s and profile %s: %v", x509Root, profile, err)
+	}
+
+	return content, nil
 }
 
 func (p *CommandProcessor) signOpenpgpKey(system *IdSystemParameters, days uint16, pubKey []byte) ([]byte, error) {
diff --git a/signer/protocol_elements.go b/signer/protocol_elements.go
index c39710e..eca525a 100644
--- a/signer/protocol_elements.go
+++ b/signer/protocol_elements.go
@@ -19,25 +19,31 @@ const (
 const (
 	X509RootDefault shared.CryptoSystemRootId = 0
 	X509RootClass3  shared.CryptoSystemRootId = 1
-	X509RootClass3s shared.CryptoSystemRootId = 2
-	X509Root3       shared.CryptoSystemRootId = 3
-	X509Root4       shared.CryptoSystemRootId = 4
-	X509Root5       shared.CryptoSystemRootId = 5
+	// The following roots existed in the old server.pl but had
+	// no profile configurations and were thus unusable
+	//
+	// X509RootClass3s shared.CryptoSystemRootId = 2
+	// X509Root3       shared.CryptoSystemRootId = 3
+	// X509Root4       shared.CryptoSystemRootId = 4
+	// X509Root5       shared.CryptoSystemRootId = 5
 )
 
 const (
 	X509ProfileClient         shared.CertificateProfileId = 0
 	X509ProfileClientOrg      shared.CertificateProfileId = 1
 	X509ProfileClientCodesign shared.CertificateProfileId = 2
-	X509ProfileClientMachine  shared.CertificateProfileId = 3
-	X509ProfileClientAds      shared.CertificateProfileId = 4
 	X509ProfileServer         shared.CertificateProfileId = 5
 	X509ProfileServerOrg      shared.CertificateProfileId = 6
-	X509ProfileServerJabber   shared.CertificateProfileId = 7
 	X509ProfileOCSP           shared.CertificateProfileId = 8
 	X509ProfileTimestamp      shared.CertificateProfileId = 9
-	X509ProfileProxy          shared.CertificateProfileId = 10
-	X509ProfileSubCA          shared.CertificateProfileId = 11
+
+	// the following profiles where valid options in the original signer code but had no configurations
+	//
+	// X509ProfileClientMachine  shared.CertificateProfileId = 3  // no configuration on original signer
+	// X509ProfileClientAds      shared.CertificateProfileId = 4  // no configuration on original signer
+	// X509ProfileServerJabber   shared.CertificateProfileId = 7  // no configuration on original signer
+	// X509ProfileProxy          shared.CertificateProfileId = 10  // no configuration on original signer
+	// X509ProfileSubCA          shared.CertificateProfileId = 11  // no configuration on original signer
 )
 
 const (
@@ -65,30 +71,172 @@ const (
 func NewCommandProcessor() *CommandProcessor {
 	settings := NewCommandProcessorSettings()
 
+	clientPrototype := &x509.Certificate{
+		KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
+		ExtKeyUsage: []x509.ExtKeyUsage{
+			x509.ExtKeyUsageEmailProtection,
+			x509.ExtKeyUsageClientAuth,
+			// x509.ExtKeyUsageMicrosoftServerGatedCrypto,
+			// 1.3.6.1.4.1.311.10.3.4 msEFS not supported by golang.org/crypto
+			// x509.ExtKeyUsageNetscapeServerGatedCrypto,
+		},
+	}
+	codeSignPrototype := &x509.Certificate{
+		KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
+		ExtKeyUsage: []x509.ExtKeyUsage{
+			x509.ExtKeyUsageEmailProtection,
+			x509.ExtKeyUsageClientAuth,
+			x509.ExtKeyUsageCodeSigning,
+			// 1.3.6.1.4.1.311.2.1.21 msCodeInd not supported by golang.org/crypto
+			// x509.ExtKeyUsageMicrosoftCommercialCodeSigning,
+			// x509.ExtKeyUsageMicrosoftServerGatedCrypto,
+			// 1.3.6.1.4.1.311.10.3.4 msEFS not supported by golang.org/crypto
+			// x509.ExtKeyUsageNetscapeServerGatedCrypto,
+		},
+	}
+	serverPrototype := &x509.Certificate{
+		KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
+		ExtKeyUsage: []x509.ExtKeyUsage{
+			x509.ExtKeyUsageClientAuth,
+			x509.ExtKeyUsageServerAuth,
+			// x509.ExtKeyUsageMicrosoftServerGatedCrypto,
+			// x509.ExtKeyUsageNetscapeServerGatedCrypto,
+		},
+	}
+	ocspPrototype := &x509.Certificate{
+		KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
+		ExtKeyUsage: []x509.ExtKeyUsage{
+			x509.ExtKeyUsageServerAuth,
+			x509.ExtKeyUsageOCSPSigning,
+			// x509.ExtKeyUsageMicrosoftServerGatedCrypto,
+			// x509.ExtKeyUsageNetscapeServerGatedCrypto,
+		},
+	}
+	timestampPrototype := &x509.Certificate{
+		KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
+		ExtKeyUsage: []x509.ExtKeyUsage{
+			x509.ExtKeyUsageServerAuth,
+			x509.ExtKeyUsageOCSPSigning,
+			// x509.ExtKeyUsageMicrosoftServerGatedCrypto,
+			// x509.ExtKeyUsageNetscapeServerGatedCrypto,
+		},
+	}
 	cryptoSystems := map[shared.CryptoSystemId]*CryptoSystem{
 		CsX509: {
 			Name: "X.509",
 			Roots: map[shared.CryptoSystemRootId]interface{}{
-				X509RootDefault: x509_ops.NewRoot(settings.CABaseDir, "openssl", "CA", X509RootDefault),
-				X509RootClass3:  x509_ops.NewRoot(settings.CABaseDir, "class3", "class3", X509RootClass3),
-				X509RootClass3s: &x509_ops.Root{Name: "class3s"},
-				X509Root3:       &x509_ops.Root{Name: "root3"},
-				X509Root4:       &x509_ops.Root{Name: "root4"},
-				X509Root5:       &x509_ops.Root{Name: "root5"},
+				X509RootDefault: x509_ops.NewRoot(
+					settings.CABaseDir,
+					"openssl",
+					"CA",
+					X509RootDefault,
+					// TODO: parse crl distribution points from configuration
+					[]string{"http://crl.cacert.localhost/revoke.crl"},
+					// TODO: parse OCSP endpoints from configuration
+					[]string{"http://ocsp.cacert.localhost"},
+				),
+				X509RootClass3: x509_ops.NewRoot(
+					settings.CABaseDir,
+					"class3",
+					"class3",
+					X509RootClass3,
+					// TODO: parse crl distribution points from configuration
+					[]string{"http://crl.cacert.localhost/class3-revoke.crl"},
+					// TODO: parse OCSP endpoints from configuration
+					[]string{"http://ocsp.cacert.localhost"},
+				),
+				// The following roots existed in the old server.pl but had
+				// no profile configurations and were thus unusable
+				//
+				// X509RootClass3s: &x509_ops.Root{Name: "class3s"},  // no profile configs
+				// X509Root3:       &x509_ops.Root{Name: "root3"},
+				// X509Root4:       &x509_ops.Root{Name: "root4"},
+				// X509Root5:       &x509_ops.Root{Name: "root5"},
 			},
 			Profiles: map[shared.CertificateProfileId]interface{}{
-				X509ProfileClient:         &x509_ops.Profile{Name: "client"},
-				X509ProfileClientOrg:      &x509_ops.Profile{Name: "client-org"},
-				X509ProfileClientCodesign: &x509_ops.Profile{Name: "client-codesign"},
-				X509ProfileClientMachine:  &x509_ops.Profile{Name: "client-machine"},
-				X509ProfileClientAds:      &x509_ops.Profile{Name: "client-ads"},
-				X509ProfileServer:         &x509_ops.Profile{Name: "server"},
-				X509ProfileServerOrg:      &x509_ops.Profile{Name: "server-org"},
-				X509ProfileServerJabber:   &x509_ops.Profile{Name: "server-jabber"},
-				X509ProfileOCSP:           &x509_ops.Profile{Name: "ocsp"},
-				X509ProfileTimestamp:      &x509_ops.Profile{Name: "timestamp"},
-				X509ProfileProxy:          &x509_ops.Profile{Name: "proxy"},
-				X509ProfileSubCA:          &x509_ops.Profile{Name: "subca"},
+				X509ProfileClient: x509_ops.NewProfile(
+					"client",
+					clientPrototype,
+					[]x509_ops.SubjectDnField{
+						x509_ops.SubjectDnFieldCommonName,
+						x509_ops.SubjectDnFieldEmailAddress,
+					},
+					nil,
+					true,
+				),
+				X509ProfileClientOrg: x509_ops.NewProfile("client-org", clientPrototype,
+					[]x509_ops.SubjectDnField{
+						x509_ops.SubjectDnFieldCountryName,
+						x509_ops.SubjectDnFieldStateOrProvinceName,
+						x509_ops.SubjectDnFieldLocalityName,
+						x509_ops.SubjectDnFieldOrganizationName,
+						x509_ops.SubjectDnFieldOrganizationalUnitName,
+						x509_ops.SubjectDnFieldCommonName,
+						x509_ops.SubjectDnFieldEmailAddress,
+					},
+					nil,
+					true,
+				),
+				X509ProfileClientCodesign: x509_ops.NewProfile("client-codesign", codeSignPrototype,
+					[]x509_ops.SubjectDnField{
+						x509_ops.SubjectDnFieldCountryName,
+						x509_ops.SubjectDnFieldStateOrProvinceName,
+						x509_ops.SubjectDnFieldLocalityName,
+						x509_ops.SubjectDnFieldCommonName,
+						x509_ops.SubjectDnFieldEmailAddress,
+					},
+					nil,
+					true,
+				),
+				// X509ProfileClientMachine:  &x509_ops.Profile{Name: "client-machine"},
+				// X509ProfileClientAds:      &x509_ops.Profile{Name: "client-ads"},
+				X509ProfileServer: x509_ops.NewProfile("server", serverPrototype,
+					[]x509_ops.SubjectDnField{
+						x509_ops.SubjectDnFieldCommonName,
+					},
+					[]x509_ops.AltNameType{x509_ops.NameTypeDNS, x509_ops.NameTypeXmppJid},
+					false,
+				),
+				X509ProfileServerOrg: x509_ops.NewProfile("server-org", serverPrototype,
+					[]x509_ops.SubjectDnField{
+						x509_ops.SubjectDnFieldCountryName,
+						x509_ops.SubjectDnFieldStateOrProvinceName,
+						x509_ops.SubjectDnFieldLocalityName,
+						x509_ops.SubjectDnFieldOrganizationName,
+						x509_ops.SubjectDnFieldOrganizationalUnitName,
+						x509_ops.SubjectDnFieldCommonName,
+					},
+					[]x509_ops.AltNameType{x509_ops.NameTypeDNS, x509_ops.NameTypeXmppJid},
+					false,
+				),
+				// X509ProfileServerJabber: &x509_ops.Profile{Name: "server-jabber"},
+				X509ProfileOCSP: x509_ops.NewProfile("ocsp", ocspPrototype,
+					[]x509_ops.SubjectDnField{
+						x509_ops.SubjectDnFieldCountryName,
+						x509_ops.SubjectDnFieldStateOrProvinceName,
+						x509_ops.SubjectDnFieldLocalityName,
+						x509_ops.SubjectDnFieldOrganizationName,
+						x509_ops.SubjectDnFieldOrganizationalUnitName,
+						x509_ops.SubjectDnFieldCommonName,
+						x509_ops.SubjectDnFieldEmailAddress,
+					},
+					nil,
+					false,
+				),
+				X509ProfileTimestamp: x509_ops.NewProfile("timestamp", timestampPrototype,
+					[]x509_ops.SubjectDnField{
+						x509_ops.SubjectDnFieldCountryName,
+						x509_ops.SubjectDnFieldStateOrProvinceName,
+						x509_ops.SubjectDnFieldLocalityName,
+						x509_ops.SubjectDnFieldOrganizationName,
+						x509_ops.SubjectDnFieldOrganizationalUnitName,
+						x509_ops.SubjectDnFieldCommonName,
+					},
+					nil,
+					true,
+				),
+				// X509ProfileProxy:          &x509_ops.Profile{Name: "proxy"},
+				// X509ProfileSubCA:          &x509_ops.Profile{Name: "subca"},
 			},
 			// constants for openssl invocations. Should be replaced with
 			// something more useful
diff --git a/signer/x509_ops/operations.go b/signer/x509_ops/operations.go
index 407e281..868f9c5 100644
--- a/signer/x509_ops/operations.go
+++ b/signer/x509_ops/operations.go
@@ -2,15 +2,18 @@ package x509_ops
 
 import (
 	"bufio"
+	"bytes"
 	"crypto"
 	"crypto/rand"
 	"crypto/rsa"
 	"crypto/sha1"
 	"crypto/x509"
 	"crypto/x509/pkix"
+	"encoding/base64"
 	"encoding/hex"
 	"encoding/pem"
 	"fmt"
+	"io"
 	"io/ioutil"
 	"math/big"
 	"os"
@@ -20,6 +23,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/longsleep/pkac"
 	log "github.com/sirupsen/logrus"
 
 	"git.cacert.org/cacert-gosigner/shared"
@@ -28,26 +32,28 @@ import (
 
 const crlLifetime = time.Hour * 24 * 7
 
+var (
+	oidPkcs9EmailAddress = []int{1, 2, 840, 113549, 1, 9, 1}
+)
+
 type Root struct {
-	Name            string
-	privateKeyFile  string
-	certificateFile string
-	databaseFile    string
-	crlNumberFile   string
-	crlFileName     string
-	crlHashDir      string
+	Name                  string
+	privateKey            crypto.Signer
+	certificate           *x509.Certificate
+	databaseFile          string
+	serialNumberFile      string
+	crlNumberFile         string
+	crlFileName           string
+	crlHashDir            string
+	crlDistributionPoints []string
+	ocspServers           []string
 }
 
 func (x *Root) String() string {
 	return x.Name
 }
 
-type Profile struct {
-	Name string
-}
-
-func (x *Root) loadCertificate() (*x509.Certificate, error) {
-	certificateFile := x.certificateFile
+func loadCertificate(certificateFile string) (*x509.Certificate, error) {
 	pemBytes, err := ioutil.ReadFile(certificateFile)
 	if err != nil {
 		return nil, fmt.Errorf(
@@ -75,21 +81,23 @@ func (x *Root) loadCertificate() (*x509.Certificate, error) {
 	return certificate, nil
 }
 
-func (x *Root) getPrivateKey() (crypto.Signer, error) {
-	privateKeyFile := x.privateKeyFile
-	pemBytes, err := ioutil.ReadFile(privateKeyFile)
+func loadPrivateKey(filename string) (crypto.Signer, error) {
+	pemBytes, err := ioutil.ReadFile(filename)
 	if err != nil {
 		return nil, fmt.Errorf(
 			"could not load private key %s: %v",
-			privateKeyFile,
+			filename,
 			err,
 		)
 	}
 	pemBlock, _ := pem.Decode(pemBytes)
+	if pemBlock == nil {
+		return nil, fmt.Errorf("no PEM data found in %s", filename)
+	}
 	if pemBlock.Type != "PRIVATE KEY" {
 		log.Warnf(
 			"PEM in %s is probably not a private key. PEM block has type %s",
-			privateKeyFile,
+			filename,
 			pemBlock.Type,
 		)
 	}
@@ -97,27 +105,46 @@ func (x *Root) getPrivateKey() (crypto.Signer, error) {
 	if err != nil {
 		return nil, fmt.Errorf(
 			"could no parse private key from %s: %v",
-			privateKeyFile,
+			filename,
 			err,
 		)
 	}
 	return privateKey.(*rsa.PrivateKey), nil
 }
 
-func (x *Root) getNextCRLNumber() (*big.Int, error) {
-	crlNumberFile := x.crlNumberFile
-	_, err := os.Stat(crlNumberFile)
+func (x *Root) getNextSerialNumber() (*big.Int, error) {
+	// TODO: decide whether we should use 64 bit random serial numbers as
+	//       recommended by CAB forum baseline requirements
+	serialNumberFile := x.serialNumberFile
+	_, err := os.Stat(serialNumberFile)
 	if err != nil {
-		log.Warnf("CRL number file %s does not exist: %v", crlNumberFile, err)
+		log.Warnf("serial number file %s does not exist: %v", x.serialNumberFile, err)
 		return big.NewInt(1), nil
 	}
-	data, err := ioutil.ReadFile(crlNumberFile)
+	data, err := ioutil.ReadFile(x.serialNumberFile)
 	if err != nil {
-		return nil, fmt.Errorf("could not read CRL number file %s", crlNumberFile)
+		return nil, fmt.Errorf("could not read serial number file %s: %v", x.serialNumberFile, err)
 	}
 	result, err := common.StringAsBigInt(data)
 	if err != nil {
-		return nil, fmt.Errorf("could not parse content of %s as CRL number: %v", crlNumberFile, err)
+		return nil, fmt.Errorf("could not parse content of %s as serial number: %v", x.serialNumberFile, err)
+	}
+	return result, err
+}
+
+func (x *Root) getNextCRLNumber() (*big.Int, error) {
+	_, err := os.Stat(x.crlNumberFile)
+	if err != nil {
+		log.Warnf("CRL number file %s does not exist: %v", x.crlNumberFile, err)
+		return big.NewInt(1), nil
+	}
+	data, err := ioutil.ReadFile(x.crlNumberFile)
+	if err != nil {
+		return nil, fmt.Errorf("could not read CRL number file %s: %v", x.crlNumberFile, err)
+	}
+	result, err := common.StringAsBigInt(data)
+	if err != nil {
+		return nil, fmt.Errorf("could not parse content of %s as CRL number: %v", x.crlNumberFile, err)
 	}
 	return result, nil
 }
@@ -148,6 +175,33 @@ func (x *Root) bumpCRLNumber(current *big.Int) error {
 	return nil
 }
 
+func (x *Root) bumpSerialNumber(current *big.Int) error {
+	serial := current.Int64() + 1
+	outFile, err := ioutil.TempFile(path.Dir(x.serialNumberFile), "*.txt")
+	defer func() { _ = outFile.Close() }()
+
+	_, err = outFile.WriteString(fmt.Sprintf(
+		"%s\n",
+		strings.ToUpper(strconv.FormatInt(serial, 16)),
+	))
+	if err != nil {
+		return fmt.Errorf("could not write new serial number %d to %s: %v", serial, outFile.Name(), err)
+	}
+
+	if err = outFile.Close(); err != nil {
+		return fmt.Errorf("could not close temporary file %s: %v", outFile.Name(), err)
+	}
+	if _, err = os.Stat(x.serialNumberFile); err == nil {
+		if err = os.Rename(x.serialNumberFile, fmt.Sprintf("%s.old", x.serialNumberFile)); err != nil {
+			return fmt.Errorf("could not rename %s to %s.old: %v", x.serialNumberFile, x.serialNumberFile, err)
+		}
+	}
+	if err = os.Rename(outFile.Name(), x.serialNumberFile); err != nil {
+		return fmt.Errorf("could not rename %s to %s: %v", outFile.Name(), x.serialNumberFile, err)
+	}
+	return nil
+}
+
 func (x *Root) loadRevokedCertificatesFromDatabase() ([]pkix.RevokedCertificate, error) {
 	databaseFile := x.databaseFile
 	_, err := os.Stat(databaseFile)
@@ -182,18 +236,17 @@ func (x *Root) loadRevokedCertificatesFromDatabase() ([]pkix.RevokedCertificate,
 }
 
 func (x *Root) recordRevocation(certificate *x509.Certificate) (*pkix.RevokedCertificate, error) {
-	databaseFile := x.databaseFile
-	_, err := os.Stat(databaseFile)
+	_, err := os.Stat(x.databaseFile)
 	if err != nil {
-		return nil, fmt.Errorf("openssl certificate database file %s does not exist: %v", databaseFile, err)
+		return nil, fmt.Errorf("openssl certificate database file %s does not exist: %v", x.databaseFile, err)
 	}
-	inFile, err := os.Open(databaseFile)
+	inFile, err := os.Open(x.databaseFile)
 	if err != nil {
-		return nil, fmt.Errorf("could not open openssl certificate database file %s: %v", databaseFile, err)
+		return nil, fmt.Errorf("could not open openssl certificate database file %s: %v", x.databaseFile, err)
 	}
 	defer func() { _ = inFile.Close() }()
 
-	outFile, err := ioutil.TempFile(path.Dir(databaseFile), "*.txt")
+	outFile, err := ioutil.TempFile(path.Dir(x.databaseFile), "*.txt")
 	defer func() { _ = outFile.Close() }()
 
 	scanner := bufio.NewScanner(inFile)
@@ -225,15 +278,15 @@ func (x *Root) recordRevocation(certificate *x509.Certificate) (*pkix.RevokedCer
 		return nil, fmt.Errorf("could not close temporary file %s: %v", outFile.Name(), err)
 	}
 	if err = inFile.Close(); err != nil {
-		return nil, fmt.Errorf("could not close %s: %v", databaseFile, err)
+		return nil, fmt.Errorf("could not close %s: %v", x.databaseFile, err)
 	}
 
-	if err = os.Rename(databaseFile, fmt.Sprintf("%s.old", databaseFile)); err != nil {
-		return nil, fmt.Errorf("could not rename %s to %s.old: %v", databaseFile, databaseFile, err)
+	if err = os.Rename(x.databaseFile, fmt.Sprintf("%s.old", x.databaseFile)); err != nil {
+		return nil, fmt.Errorf("could not rename %s to %s.old: %v", x.databaseFile, x.databaseFile, err)
 	}
 
-	if err = os.Rename(outFile.Name(), databaseFile); err != nil {
-		return nil, fmt.Errorf("could not rename temporary file %s to %s: %v", outFile.Name(), databaseFile, err)
+	if err = os.Rename(outFile.Name(), x.databaseFile); err != nil {
+		return nil, fmt.Errorf("could not rename temporary file %s to %s: %v", outFile.Name(), x.databaseFile, err)
 	}
 
 	if !found {
@@ -268,14 +321,6 @@ func (x *Root) RevokeCertificate(request []byte) (*pkix.RevokedCertificate, erro
 }
 
 func (x *Root) GenerateCrl(algorithm x509.SignatureAlgorithm) ([]byte, *[20]byte, error) {
-	caCertificate, err := x.loadCertificate()
-	if err != nil {
-		return nil, nil, err
-	}
-	caPrivateKey, err := x.getPrivateKey()
-	if err != nil {
-		return nil, nil, err
-	}
 	certificatesToRevoke, err := x.loadRevokedCertificatesFromDatabase()
 	if err != nil {
 		return nil, nil, err
@@ -302,8 +347,8 @@ func (x *Root) GenerateCrl(algorithm x509.SignatureAlgorithm) ([]byte, *[20]byte
 	crlBytes, err := x509.CreateRevocationList(
 		rand.Reader,
 		crlTemplate,
-		caCertificate,
-		caPrivateKey,
+		x.certificate,
+		x.privateKey,
 	)
 	if err != nil {
 		return nil, nil, fmt.Errorf("could not create new CRL: %v", err)
@@ -355,11 +400,10 @@ func (x *Root) GetCrlFileName(hash string) string {
 
 func (x *Root) checkPreconditions() {
 	results := []bool{
-		x.checkFile(x.privateKeyFile, "private key"),
-		x.checkFile(x.certificateFile, "certificate"),
 		x.checkFile(x.databaseFile, "database"),
+		x.checkFile(x.serialNumberFile, "serial number"),
 		x.checkFile(x.crlNumberFile, "CRL serial number"),
-		x.checkFile(x.crlFileName, "CRL file"),
+		x.checkFile(x.crlFileName, "CRL"),
 		x.checkDir(x.crlHashDir, "directory for hash indexed CRLs"),
 	}
 	for _, success := range results {
@@ -399,20 +443,418 @@ func (x *Root) checkDir(path, prefix string) bool {
 	return ok
 }
 
-func NewRoot(basedir, name, subdir string, id shared.CryptoSystemRootId) *Root {
+type SigningRequestParameters struct {
+	Request                 []byte
+	Subject                 []byte
+	SubjectAlternativeNames []byte
+	Days                    uint16
+	IsSpkac                 bool
+}
+
+func (x *Root) SignCertificate(
+	profile *Profile,
+	algorithm x509.SignatureAlgorithm,
+	params *SigningRequestParameters,
+) ([]byte, error) {
+	var publicKey interface{}
+	if params.IsSpkac {
+		var err error
+		const spkacPrefix = "SPKAC="
+		if bytes.Compare([]byte(spkacPrefix), params.Request[:len(spkacPrefix)]) != 0 {
+			return nil, fmt.Errorf("request does not contain a valid SPKAC string")
+		}
+		derBytes, err := base64.StdEncoding.DecodeString(string(params.Request[len(spkacPrefix):]))
+		if err != nil {
+			return nil, fmt.Errorf("could not decode SPKAC bytes: %v", err)
+		}
+		publicKey, err = pkac.ParseSPKAC(derBytes)
+		if err != nil {
+			return nil, fmt.Errorf("could not parse SPKAC: %v", err)
+		}
+	} else {
+		csrBlock, _ := pem.Decode(params.Request)
+		if csrBlock.Type != "CERTIFICATE REQUEST" {
+			return nil, fmt.Errorf("unexpected PEM block '%s' instead of 'CERTIFICATE REQUEST'", csrBlock.Type)
+		}
+		csr, err := x509.ParseCertificateRequest(csrBlock.Bytes)
+		if err != nil {
+			return nil, fmt.Errorf("could not parse CSR: %v", err)
+		}
+		publicKey = csr.PublicKey
+	}
+
+	nextSerialNumber, err := x.getNextSerialNumber()
+	if err != nil {
+		return nil, fmt.Errorf("could not get next serial number: %v", err)
+	}
+
+	// copy profile
+	notBefore := time.Now()
+	notAfter := notBefore.Add(time.Hour * 24 * time.Duration(params.Days))
+	certificate := &x509.Certificate{
+		KeyUsage:              profile.prototype.KeyUsage,
+		ExtKeyUsage:           profile.prototype.ExtKeyUsage,
+		SignatureAlgorithm:    algorithm,
+		CRLDistributionPoints: x.crlDistributionPoints,
+		OCSPServer:            x.ocspServers,
+		SerialNumber:          nextSerialNumber,
+		NotBefore:             notBefore,
+		NotAfter:              notAfter,
+	}
+
+	// check subject
+	subject, err := profile.parseSubject(params.Subject)
+	if err != nil {
+		return nil, fmt.Errorf("could not parse subject: %v", err)
+	}
+	certificate.Subject = *subject
+
+	// check altNames
+	err = profile.parseAltNames(certificate, params.SubjectAlternativeNames)
+	if err != nil {
+		return nil, fmt.Errorf("could not parse subject alternative names: %v", err)
+	}
+
+	moveEmailsFromSubjectToAlternativeNames(certificate)
+
+	log.Tracef("prepared for signing: %+v", certificate)
+
+	certBytes, err := x509.CreateCertificate(rand.Reader, certificate, x.certificate, publicKey, x.privateKey)
+	if err != nil {
+		return nil, fmt.Errorf("could not sign certificate: %v", err)
+	}
+
+	parsedCertificate, err := x509.ParseCertificate(certBytes)
+	if err != nil {
+		return nil, fmt.Errorf("could not parse signed certificate: %v", err)
+	}
+
+	if err = x.bumpSerialNumber(nextSerialNumber); err != nil {
+		log.Errorf("could not bump serial number: %v", err)
+	}
+
+	err = x.recordIssuedCertificate(parsedCertificate)
+	if err != nil {
+		return nil, fmt.Errorf("could not record signed certificate in database: %v", err)
+	}
+
+	pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
+	log.Tracef("signed new certificate\n%s", pemBytes)
+
+	return pemBytes, nil
+}
+
+// we move the email addresses to alt names because they are encoded to UTF-8
+// otherwise which is not compliant to RFC-5280
+func moveEmailsFromSubjectToAlternativeNames(certificate *x509.Certificate) {
+	extraNames := make([]pkix.AttributeTypeAndValue, 0)
+	for _, p := range certificate.Subject.ExtraNames {
+		if p.Type.Equal(oidPkcs9EmailAddress) {
+			email := p.Value.(string)
+			if certificate.EmailAddresses == nil {
+				certificate.EmailAddresses = []string{email}
+				continue
+			}
+			for _, e := range certificate.EmailAddresses {
+				if e == p.Value {
+					continue
+				}
+			}
+			certificate.EmailAddresses = append(certificate.EmailAddresses, email)
+		} else {
+			extraNames = append(extraNames, p)
+		}
+	}
+	certificate.Subject.ExtraNames = extraNames
+}
+
+func (x *Root) recordIssuedCertificate(certificate *x509.Certificate) error {
+	log.Tracef("recording %+v", certificate)
+	tempFile, err := ioutil.TempFile(path.Dir(x.databaseFile), "*.txt")
+	if err != nil {
+		return fmt.Errorf("could not create temporary file: %v", err)
+	}
+	defer func() { _ = tempFile.Close() }()
+	tempName := tempFile.Name()
+
+	dbExists := false
+	_, err = os.Stat(x.databaseFile)
+	if err != nil {
+		log.Warnf("openssl certificate database file %s does not exist: %v", x.databaseFile, err)
+	} else {
+		dbExists = true
+		inFile, err := os.Open(x.databaseFile)
+		defer func() { _ = inFile.Close() }()
+		if err != nil {
+			return fmt.Errorf("could not open openssl certificate database file %s: %v", x.databaseFile, err)
+		}
+		_, err = io.Copy(tempFile, inFile)
+		if err != nil {
+			return fmt.Errorf("could not copy %s to temporary file %s: %v", x.databaseFile, tempName, err)
+		}
+		if err = inFile.Close(); err != nil {
+			return fmt.Errorf("could not close %s: %v", x.databaseFile, err)
+		}
+	}
+	if err = tempFile.Close(); err != nil {
+		return fmt.Errorf("could not close temporary file %s: %v", tempName, err)
+	}
+	outFile, err := os.OpenFile(tempName, os.O_APPEND|os.O_WRONLY, 0644)
+	if err != nil {
+		return fmt.Errorf("could not open temporary file for writing %s: %v", tempName, err)
+	}
+	defer func() { _ = outFile.Close() }()
+	line := strings.Join([]string{"V", strconv.FormatInt(certificate.NotBefore.Unix(), 10) + "Z", "", strings.ToUpper(certificate.SerialNumber.Text(16)), "unknown", opensslFormatDN(certificate.Subject)}, "\t")
+	_, err = fmt.Fprintln(outFile, line)
+	if err != nil {
+		return fmt.Errorf("could not write '%s' to %s: %v", line, tempName, err)
+	}
+	if err = outFile.Close(); err != nil {
+		return fmt.Errorf("could not close temporary file %s: %v", tempName, err)
+	}
+
+	if dbExists {
+		if err = os.Rename(x.databaseFile, fmt.Sprintf("%s.old", x.databaseFile)); err != nil {
+			return fmt.Errorf("could not rename %s to %s.old: %v", x.databaseFile, x.databaseFile, err)
+		}
+	}
+
+	if err = os.Rename(tempName, x.databaseFile); err != nil {
+		return fmt.Errorf("could not rename temporary file %s to %s: %v", tempName, x.databaseFile, err)
+	}
+
+	return nil
+}
+
+func opensslFormatDN(subject pkix.Name) string {
+	var buf strings.Builder
+	for _, rdn := range subject.ToRDNSequence() {
+		if len(rdn) == 0 {
+			continue
+		}
+		for _, atv := range rdn {
+			value, ok := atv.Value.(string)
+			if !ok {
+				continue
+			}
+			t := atv.Type
+			if len(t) == 4 && t[:3].Equal([]int{2, 5, 4}) {
+				switch t[3] {
+				case 3:
+					buf.WriteString("/CN=")
+					buf.WriteString(value)
+				case 6:
+					buf.WriteString("/C=")
+					buf.WriteString(value)
+				case 7:
+					buf.WriteString("/L=")
+					buf.WriteString(value)
+				case 8:
+					buf.WriteString("/ST=")
+					buf.WriteString(value)
+				case 10:
+					buf.WriteString("/O=")
+					buf.WriteString(value)
+				case 11:
+					buf.WriteString("/OU=")
+					buf.WriteString(value)
+				}
+			} else if t.Equal(oidPkcs9EmailAddress) {
+				buf.WriteString("/emailAddress=")
+				buf.WriteString(value)
+			}
+		}
+	}
+	return buf.String()
+}
+
+func NewRoot(
+	basedir, name, subdir string,
+	id shared.CryptoSystemRootId,
+	crlDistributionPoints, ocspServers []string,
+) *Root {
+	key, err := loadPrivateKey(path.Join(basedir, subdir, "private", "ca.key.pem"))
+	if err != nil {
+		log.Fatalf("could not load private key: %v", err)
+	}
+	cert, err := loadCertificate(path.Join(basedir, subdir, "ca.crt.pem"))
+	if err != nil {
+		log.Fatalf("could not load CA certificate: %v", err)
+	}
 	root := &Root{
-		Name:            name,
-		privateKeyFile:  path.Join(basedir, subdir, "private", "ca.key.pem"),
-		certificateFile: path.Join(basedir, subdir, "ca.crt.pem"),
-		databaseFile:    path.Join(basedir, subdir, "index.txt"),
-		crlNumberFile:   path.Join(basedir, subdir, "crlnumber"),
-		crlFileName:     fmt.Sprintf("revoke-root%d.crl", id),
+		Name:             name,
+		privateKey:       key,
+		certificate:      cert,
+		databaseFile:     path.Join(basedir, subdir, "index.txt"),
+		serialNumberFile: path.Join(basedir, subdir, "serial"),
+		crlNumberFile:    path.Join(basedir, subdir, "crlnumber"),
+		crlFileName:      fmt.Sprintf("revoke-root%d.crl", id),
 		crlHashDir: path.Join(
 			basedir,
 			"currentcrls",
 			fmt.Sprintf("%d", id),
 		),
+		crlDistributionPoints: crlDistributionPoints,
+		ocspServers:           ocspServers,
 	}
 	root.checkPreconditions()
 	return root
 }
+
+type AltNameType string
+
+const (
+	NameTypeDNS     AltNameType = "DNS"
+	NameTypeXmppJid             = "otherName:1.3.6.1.5.5.7.8.5;UTF8" // from RFC 3920, 6120
+)
+
+type SubjectDnField string
+
+const (
+	SubjectDnFieldCountryName            SubjectDnField = "C"
+	SubjectDnFieldStateOrProvinceName                   = "ST"
+	SubjectDnFieldLocalityName                          = "L"
+	SubjectDnFieldOrganizationName                      = "O"
+	SubjectDnFieldOrganizationalUnitName                = "OU"
+	SubjectDnFieldCommonName                            = "CN"
+	SubjectDnFieldEmailAddress                          = "emailAddress"
+)
+
+type Profile struct {
+	name            string
+	prototype       *x509.Certificate
+	subjectDNFields []SubjectDnField
+	altNameTypes    []AltNameType
+	copyEmail       bool // whether to copy emailAddress values from subjectDN to email address subject alternative name
+}
+
+func (p *Profile) String() string {
+	return p.name
+}
+
+func (p *Profile) parseSubject(subject []byte) (*pkix.Name, error) {
+
+	parts := strings.Split(string(subject), "/")
+	subjectDN := &pkix.Name{}
+	for _, part := range parts {
+		if len(strings.TrimSpace(part)) == 0 {
+			continue
+		}
+		handled := false
+		item := strings.SplitN(part, "=", 2)
+		for _, f := range p.subjectDNFields {
+			if strings.ToUpper(item[0]) != strings.ToUpper(string(f)) {
+				continue
+			}
+			value := item[1]
+			handled = true
+			switch f {
+			case SubjectDnFieldCountryName:
+				if subjectDN.Country == nil {
+					subjectDN.Country = []string{value}
+				} else {
+					subjectDN.Country = append(subjectDN.Country, value)
+				}
+			case SubjectDnFieldStateOrProvinceName:
+				if subjectDN.Province == nil {
+					subjectDN.Province = []string{value}
+				} else {
+					subjectDN.Province = append(subjectDN.Province, value)
+				}
+			case SubjectDnFieldLocalityName:
+				if subjectDN.Locality == nil {
+					subjectDN.Locality = []string{value}
+				} else {
+					subjectDN.Locality = append(subjectDN.Locality, value)
+				}
+			case SubjectDnFieldOrganizationName:
+				if subjectDN.Organization == nil {
+					subjectDN.Organization = []string{value}
+				} else {
+					subjectDN.Organization = append(subjectDN.Organization, value)
+				}
+			case SubjectDnFieldOrganizationalUnitName:
+				if subjectDN.OrganizationalUnit == nil {
+					subjectDN.OrganizationalUnit = []string{value}
+				} else {
+					subjectDN.OrganizationalUnit = append(subjectDN.OrganizationalUnit, value)
+				}
+			case SubjectDnFieldCommonName:
+				subjectDN.CommonName = value
+			case SubjectDnFieldEmailAddress:
+				emailIA5 := pkix.AttributeTypeAndValue{
+					Type:  oidPkcs9EmailAddress,
+					Value: value,
+				}
+				if subjectDN.ExtraNames == nil {
+					subjectDN.ExtraNames = []pkix.AttributeTypeAndValue{emailIA5}
+				} else {
+					subjectDN.ExtraNames = append(subjectDN.ExtraNames, emailIA5)
+				}
+			default:
+				log.Warnf("unhandled subject DN type %s", f)
+			}
+		}
+		if !handled {
+			return nil, fmt.Errorf("skipped part %s because it is not supported by profile %s", part, p)
+		}
+	}
+	log.Debugf("created subject DN %s", subjectDN)
+	return subjectDN, nil
+}
+
+func (p *Profile) parseAltNames(template *x509.Certificate, altNames []byte) error {
+	parts := strings.Split(string(altNames), ",")
+	for _, part := range parts {
+		if len(strings.TrimSpace(part)) == 0 {
+			continue
+		}
+		handled := false
+		item := strings.SplitN(part, ":", 3)
+		if item[0] == "otherName" {
+			item = []string{strings.Join(item[:2], ":"), item[2]}
+		} else {
+			item = []string{item[0], strings.Join(item[1:], ":")}
+		}
+		for _, f := range p.altNameTypes {
+			if item[0] != string(f) {
+				continue
+			}
+			value := item[1]
+			handled = true
+			switch f {
+			case NameTypeDNS:
+				if template.DNSNames == nil {
+					template.DNSNames = []string{value}
+				} else {
+					template.DNSNames = append(template.DNSNames, value)
+				}
+			case NameTypeXmppJid:
+				// x509.Certificate has no support for otherName alternative names
+				log.Warnf("skipping %s because it cannot be supported", part)
+			default:
+				log.Warnf("unhandled alternative name type %s", f)
+			}
+		}
+		if !handled {
+			return fmt.Errorf("skipped alternative name %s because it is not supported by profile %s", part, p)
+		}
+	}
+	return nil
+}
+
+func NewProfile(
+	name string,
+	prototype *x509.Certificate,
+	subjectDnFields []SubjectDnField,
+	altNameTypes []AltNameType,
+	copyEmail bool,
+) *Profile {
+	return &Profile{
+		name:            name,
+		prototype:       prototype,
+		subjectDNFields: subjectDnFields,
+		altNameTypes:    altNameTypes,
+		copyEmail:       copyEmail,
+	}
+}
diff --git a/signer/x509_ops/operations_test.go b/signer/x509_ops/operations_test.go
new file mode 100644
index 0000000..a2b27ca
--- /dev/null
+++ b/signer/x509_ops/operations_test.go
@@ -0,0 +1,204 @@
+package x509_ops
+
+import (
+	"crypto"
+	"crypto/x509"
+	"encoding/base64"
+	"encoding/pem"
+	"path"
+	"testing"
+)
+
+func TestRoot_SignClientCertificateWithCSR(t *testing.T) {
+	tempDir := t.TempDir()
+	root := &Root{
+		Name:                  "test",
+		privateKey:            loadTestKey(t),
+		certificate:           loadTestCACertificate(t),
+		databaseFile:          path.Join(tempDir, "index.txt"),
+		serialNumberFile:      path.Join(tempDir, "serial"),
+		crlDistributionPoints: []string{"http://crl.example.org/revoke.crl"},
+		ocspServers:           []string{"http://ocsp.example.org/"},
+	}
+
+	clientProfile := &Profile{
+		name: "client",
+		prototype: &x509.Certificate{
+			KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
+			ExtKeyUsage: []x509.ExtKeyUsage{
+				x509.ExtKeyUsageEmailProtection,
+				x509.ExtKeyUsageClientAuth,
+			},
+		},
+		subjectDNFields: []SubjectDnField{SubjectDnFieldCommonName, SubjectDnFieldEmailAddress},
+		copyEmail:       true,
+	}
+	certificate, err := root.SignCertificate(clientProfile, x509.SHA256WithRSA, &SigningRequestParameters{
+		Request:                 decodeToBytes(t, testRequest),
+		Subject:                 []byte(testClientSubject),
+		SubjectAlternativeNames: []byte(testClientSan),
+		Days:                    30,
+		IsSpkac:                 false,
+	})
+	if err != nil {
+		t.Errorf("error signing certificate: %v", err)
+		return
+	}
+
+	certificateDer, _ := pem.Decode(certificate)
+	if certificateDer.Type != "CERTIFICATE" {
+		t.Errorf("invalid PEM type '%s' instead of 'CERTIFICATE'", certificateDer.Type)
+		return
+	}
+	_, err = x509.ParseCertificate(certificateDer.Bytes)
+	if err != nil {
+		t.Errorf("could not parse generated certificate: %v", err)
+		return
+	}
+}
+
+func TestRoot_SignClientCertificateWithSPKAC(t *testing.T) {
+	root := &Root{
+		Name:                  "test",
+		privateKey:            loadTestKey(t),
+		certificate:           loadTestCACertificate(t),
+		databaseFile:          path.Join(t.TempDir(), "index.txt"),
+		serialNumberFile:      path.Join(t.TempDir(), "serial"),
+		crlDistributionPoints: []string{"http://crl.example.org/revoke.crl"},
+		ocspServers:           []string{"http://ocsp.example.org/"},
+	}
+
+	clientProfile := &Profile{
+		name: "client",
+		prototype: &x509.Certificate{
+			KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
+			ExtKeyUsage: []x509.ExtKeyUsage{
+				x509.ExtKeyUsageEmailProtection,
+				x509.ExtKeyUsageClientAuth,
+			},
+		},
+		subjectDNFields: []SubjectDnField{SubjectDnFieldCommonName, SubjectDnFieldEmailAddress},
+		copyEmail:       true,
+	}
+	certificate, err := root.SignCertificate(clientProfile, x509.SHA256WithRSA, &SigningRequestParameters{
+		Request:                 decodeToBytes(t, testSpkac),
+		Subject:                 []byte(testClientSubject),
+		SubjectAlternativeNames: []byte(testClientSan),
+		Days:                    30,
+		IsSpkac:                 true,
+	})
+
+	if err != nil {
+		t.Errorf("error signing certificate: %v", err)
+		return
+	}
+
+	certificateDer, _ := pem.Decode(certificate)
+	if certificateDer.Type != "CERTIFICATE" {
+		t.Errorf("invalid PEM type '%s' instead of 'CERTIFICATE'", certificateDer.Type)
+		return
+	}
+	_, err = x509.ParseCertificate(certificateDer.Bytes)
+	if err != nil {
+		t.Errorf("could not parse generated certificate: %v", err)
+		return
+	}
+}
+
+func TestRoot_SignServerCertificateWithCSR(t *testing.T) {
+	tempDir := t.TempDir()
+	root := &Root{
+		Name:                  "test",
+		privateKey:            loadTestKey(t),
+		certificate:           loadTestCACertificate(t),
+		databaseFile:          path.Join(tempDir, "index.txt"),
+		serialNumberFile:      path.Join(tempDir, "serial"),
+		crlDistributionPoints: []string{"http://crl.example.org/revoke.crl"},
+		ocspServers:           []string{"http://ocsp.example.org/"},
+	}
+
+	clientProfile := &Profile{
+		name: "server",
+		prototype: &x509.Certificate{
+			KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
+			ExtKeyUsage: []x509.ExtKeyUsage{
+				x509.ExtKeyUsageEmailProtection,
+				x509.ExtKeyUsageClientAuth,
+			},
+		},
+		subjectDNFields: []SubjectDnField{
+			SubjectDnFieldCountryName,
+			SubjectDnFieldStateOrProvinceName,
+			SubjectDnFieldLocalityName,
+			SubjectDnFieldOrganizationName,
+			SubjectDnFieldOrganizationalUnitName,
+			SubjectDnFieldCommonName,
+		},
+		altNameTypes: []AltNameType{NameTypeDNS, NameTypeXmppJid},
+		copyEmail:    false,
+	}
+	certificate, err := root.SignCertificate(clientProfile, x509.SHA256WithRSA, &SigningRequestParameters{
+		Request:                 decodeToBytes(t, testRequest),
+		Subject:                 []byte(testServerSubject),
+		SubjectAlternativeNames: []byte(testServerSan),
+		Days:                    30,
+		IsSpkac:                 false,
+	})
+	if err != nil {
+		t.Errorf("error signing certificate: %v", err)
+		return
+	}
+
+	certificateDer, _ := pem.Decode(certificate)
+	if certificateDer.Type != "CERTIFICATE" {
+		t.Errorf("invalid PEM type '%s' instead of 'CERTIFICATE'", certificateDer.Type)
+		return
+	}
+	_, err = x509.ParseCertificate(certificateDer.Bytes)
+	if err != nil {
+		t.Errorf("could not parse generated certificate: %v", err)
+		return
+	}
+}
+
+func loadTestKey(t *testing.T) crypto.Signer {
+	testKeyBytes := decodeToBytes(t, testCAKey)
+	key, err := x509.ParsePKCS1PrivateKey(testKeyBytes)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return key
+}
+
+func loadTestCACertificate(t *testing.T) *x509.Certificate {
+	testCertBytes := decodeToBytes(t, testCACertificate)
+	cert, err := x509.ParseCertificate(testCertBytes)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return cert
+}
+
+func decodeToBytes(t *testing.T, request string) []byte {
+	decodeString, err := base64.StdEncoding.DecodeString(request)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return decodeString
+}
+
+const testRequest = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ1ZEQ0NBVHdDQVFBd0R6RU5NQXNHQTFVRUF3d0VWR1Z6ZERDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRApnZ0VQQURDQ0FRb0NnZ0VCQU9wQ1JmMVZWQjVwK3RscVFjM25qekYyZVAydG40bGZ2NVlJU3RFMktMSmxJRDRECi9YZTRVUGdodlNuMUN3R1VzeCtQcFBDaTdZVFdQNXRKSWYwbmNLMm02YVZIWUtqcDN2K3NvMnRENkJ4V0lMSFAKS0tQSm5qbnBjU0l1Q3hKUytRU2xyMHh0aEJYYzZ2UzVWRE5Ib285VXJWRUYzSVlTd3VDTklqTWpMR25kYmpCagprNm5TUk5JZWVmeVBaVGI4MHFsVTJFZ3hJMFdFYTA1dm5sQTY5L2tQZGhmTjVRRHBHQ1NxU25GdXo1cGFmRXVIClZMOE1aQXVtVDJySkVkYnorcHRPRjBqMWZSWEQ4b1RKZ0ppQmszbGR6YlBqeFpndW5LSTl3NVcrSWdEWmxsNm8KRzM2TUN6WHNYREdkb25NbCt0K1JIbEdocjN0VDQrKzBobXVYL2pzQ0F3RUFBYUFBTUEwR0NTcUdTSWIzRFFFQgpDd1VBQTRJQkFRRFBlYnhmR3pRaWRud1pxUGdQYUliRmxNM0xsWXpROEJFbFUrYXV5bC90VjhJRC85a0dkekNxClN4N0QyRWVNVC83QmZ6L29XUjRMczVUeFpzdjR0N3RiSEUrSStFdEcwS3FnUDhNTTJPWStGckJBMXVnY3JJa2YKNmVpbXFEVkFtUFBNMHhCNUg3aFdNY1BMVUhzbW1GNlV4ajNsVXphOVQ5OXpxTWppMXlyYlpIc1pkMEM0RFd6RQo1YWtZU1hTTGNuK1F3R25LY1pvV1QwczNWZU5pMHNUK3BTNEVkdk1SbzV6Q3JUMW1SbFlYQkNqU0tpQzZEVjNpCnhyaDI2WWJqMjRKSys5dlNUR3N4RFlpMXUzOG04a1AxRVR2L0lCVnRDSVpKVmJ2eXhWbUpuemV2QnJONHpxdncKV1QvQi9jOGdrK0FQR1BKM3ZaZDUxNVhvM2QzVld4NkwKLS0tLS1FTkQgQ0VSVElGSUNBVEUgUkVRVUVTVC0tLS0tCg=="
+
+const testSpkac = "U1BLQUM9TUlJQ1FEQ0NBU2d3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRHFRa1g5VlZRZWFmclpha0hONTQ4eGRuajlyWitKWDcrV0NFclJOaWl5WlNBK0EvMTN1RkQ0SWIwcDlRc0JsTE1majZUd291MkUxaitiU1NIOUozQ3RwdW1sUjJDbzZkNy9yS05yUStnY1ZpQ3h6eWlqeVo0NTZYRWlMZ3NTVXZrRXBhOU1iWVFWM09yMHVWUXpSNktQVksxUkJkeUdFc0xnalNJekl5eHAzVzR3WTVPcDBrVFNIbm44ajJVMi9OS3BWTmhJTVNORmhHdE9iNTVRT3ZmNUQzWVh6ZVVBNlJna3FrcHhicythV254TGgxUy9ER1FMcGs5cXlSSFc4L3FiVGhkSTlYMFZ3L0tFeVlDWWdaTjVYYzJ6NDhXWUxweWlQY09WdmlJQTJaWmVxQnQrakFzMTdGd3huYUp6SmZyZmtSNVJvYTk3VStQdnRJWnJsLzQ3QWdNQkFBRVdBREFOQmdrcWhraUc5dzBCQVFRRkFBT0NBUUVBaDdqWmxaYXpPOXdHRkl3Mll1SVN5WjVYb2JjU0pSS2dMbG52UDh5cUkyVkxTdVBmdkNXOGJxKzNMWnNWcFZ6U0dhbDgwUTk5empOVy9lRm0xTElMRXZNZ0FCeEFtdk9UNlNKZURyajc4WkQyL0haazdmdHVLWU1VbkZTOWhzUWlkWmoveUttbDd4Nm9hQnlpalg5UVU0eTZYcCs3NzlhREFSenZOWTR5RGlRZmNuaFBVN1dablJyTUJZOHBSTVJVVjNKc2MvZXBQclZjOVhoaVE0KzBRNHFVQW1VNlVHNWNQeXFROVJJK2ZIL3VMbm8vZkNHUGZFVjM1NDV3WG5NeXNubC9HYlNLa0FHdVFmcm5ZT2dReFV4dUx1aHBoazVIaGtlMzNUUy9FYVlGc2JTZEhzRktoaCt4Uks3NW9wWExJTkpzNTFpYVdySUFTVmZvRTI0a3FRPT0K"
+
+const testClientSubject = "/CN=Test/emailAddress=test@example.org"
+
+const testClientSan = ""
+
+const testServerSubject = "/CN=www.example.org"
+
+const testServerSan = "DNS:www.example.org,otherName:1.3.6.1.5.5.7.8.5;UTF8:www.example.org"
+
+const testCAKey = "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"
+
+const testCACertificate = "MIIFmDCCBACgAwIBAgIUOlITUGXFrKZesL4LlawzVxLTXFYwDQYJKoZIhvcNAQELBQAwNzELMAkGA1UEBhMCQVUxFDASBgNVBAoMC0NBY2VydCBJbmMuMRIwEAYDVQQDDAlUZXN0IFJvb3QwHhcNMjEwMTA4MTMwNDM5WhcNMjYwMTA3MTMwNDM5WjA9MQswCQYDVQQGEwJBVTEUMBIGA1UECgwLQ0FjZXJ0IEluYy4xGDAWBgNVBAMMD0NsYXNzIDMgVGVzdCBDQTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAKjFj9hHPw8SeZ+Wwr9HuqEwaBr7KZatah1bcDwxC/28JYVG04oBj0BqFCvmN3ktk+RWP6qXX6o1AYvxZz4ijcBJ5pGLkmpEkkuhsq4LD/O+s9cV7YedAiJFJwD0dWCdG2JoVKRIrUSYKFmuZst20vi7jbdYFRw/s8gjpW3zSWUyC50BxS7c4v8D4r5U1qVAycx1r76PiouHtvalkTGgGrYly1Kv8FtOn7EUmvaZZITb+45YPYavGNkhIntJkb7shYCCr5AZnE91+cj4QlmT6JrKkJxCVyp8Z6NEpKzPl6FlgXrrGD7GZH15JxsMv8L6DG1U+TH6sjGGNQk8PP82kg6Av7I/F5gC+OLfhLGfiRFGIElVumNy9ionrqgom4OAfhvKiOAEJtnI12p419ZFk1AgrRjuspcUSjatMwloX5RNotrP/uL/kbh0NZWmJ35d7yEGR89P4LUCJUdgKl3Njg0aEzvHfNHV2Y8CJp64x0P8h8eCxKxzKazY0G9Aaa7QgwIDAQABo4IBlDCCAZAwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRI3ALoQ7dcRqu0gHtxaLVAv2DcOzAfBgNVHSMEGDAWgBTrwahlUMEoV1OavhMbkLcSar8PIzB3BggrBgEFBQcBAQRrMGkwNwYIKwYBBQUHMAKGK2h0dHA6Ly90ZXN0LmNhY2VydC5sb2NhbGhvc3QvY2Evcm9vdC9jYS5jcnQwLgYIKwYBBQUHMAGGImh0dHA6Ly9vY3NwLnRlc3QuY2FjZXJ0LmxvY2FsaG9zdC8wPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL2NybC50ZXN0LmNhY2VydC5sb2NhbGhvc3QvY2xhc3MzLmNybDBUBgNVHSAETTBLMEkGCCsGAQUFBwIBMD0wOwYIKwYBBQUHAgEWL2h0dHA6Ly90ZXN0LmNhY2VydC5sb2NhbGhvc3QvY2EvY2xhc3MzL2Nwcy5odG1sMA0GCSqGSIb3DQEBCwUAA4IBgQAcqK68GOxTfM9zSRbHWHchsbiyKcbxPo42se9dm/nLHT/N2XEW9Ycj5dZD8+XgoW8dVPS3uVZGj57Pr8ix3OXhMKGqcdO2QRAQaoyjw7t9dCkaJ8b7h39sY/5pFSSIdYAyyb9uPgJ1FPLueOqm3bZHVFcbiiA8/miiwGWPVEfK7zdEmFKMAkY2wYtWBeovKNVnCbuQ1Pd8CxvkCs5R9KnMfbU7bgJK8zkhlHwdtalmg2IS4yMuvYeL9S3QwL7fYcCjjTLCKwkj3frsnkRC5pGPHQ6/iVVbdsqAI70A1Uqcl15Jcpzg0Nc2EABjhbWO7gLpHpzMI5Alt+Tr+oWhe2M7wnBhuojgwASA10CnXT27GYXziIzr8d3P+T0PVLD2WcvQeEUJoQySw6W8CIkaZEZG6YBWjrAkGcO6JB+YJ5UiJOCHA6W4pmwNkGR2oh6JMQCUikaFVywb1HMIGOINOBHymj4KkuywC2w6SXMD4OqJcsCmHSNcqjFvcT/22kYCtDE="