Implement X.509 signing

This commit is contained in:
Jan Dittberner 2021-01-08 18:41:29 +01:00
parent 6cd132b3f7
commit 2fdde4024d
7 changed files with 929 additions and 90 deletions

View file

@ -90,12 +90,19 @@ func (r *SignerRequest) String() string {
r.MdAlgorithm, r.MdAlgorithm,
r.Days, r.Days,
r.Spkac, r.Spkac,
r.Content1, shorten(r.Content1),
r.Content2, shorten(r.Content2),
r.Content3, shorten(r.Content3),
) )
} }
func shorten(original []byte) []byte {
if len(original) > 20 {
return original[:20]
}
return original
}
func NewNulRequest() *SignerRequest { func NewNulRequest() *SignerRequest {
return &SignerRequest{ return &SignerRequest{
Version: shared.ProtocolVersion, Version: shared.ProtocolVersion,

1
go.mod
View file

@ -3,6 +3,7 @@ module git.cacert.org/cacert-gosigner
go 1.15 go 1.15
require ( require (
github.com/longsleep/pkac v0.0.0-20191013204540-205111305195
github.com/sirupsen/logrus v1.7.0 github.com/sirupsen/logrus v1.7.0
go.bug.st/serial v1.1.1 go.bug.st/serial v1.1.1
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad

2
go.sum
View file

@ -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/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 h1:CfOE6Sr6BvfT6R90AgKcospJGP5+hwYhOjFR1XVb68Q=
github.com/jandd/crypto v0.0.0-20210106144236-c3a8dd255ad6/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=

View file

@ -94,14 +94,14 @@ func (p *CommandProcessor) handleSignAction(
*datastructures.SignerResponse, *datastructures.SignerResponse,
error, error,
) { ) {
log.Debugf("handle sign call: %v", command) log.Debugf("handle sign call: %s", command)
idSystem, err := p.checkIdentitySystem( idSystem, err := p.checkIdentitySystem(
command.System, command.Root, command.Profile, command.MdAlgorithm) command.System, command.Root, command.Profile, command.MdAlgorithm)
if err != nil { if err != nil {
return nil, err return nil, err
} }
log.Debugf("identified id system: %+v", idSystem) log.Debugf("identified id system: %s", idSystem)
switch command.System { switch command.System {
case CsX509: case CsX509:
@ -167,6 +167,10 @@ type IdSystemParameters struct {
MessageDigestAlgorithm interface{} 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( func (p *CommandProcessor) checkIdentitySystem(
systemId shared.CryptoSystemId, systemId shared.CryptoSystemId,
rootId shared.CryptoSystemRootId, 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) { 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) { func (p *CommandProcessor) signOpenpgpKey(system *IdSystemParameters, days uint16, pubKey []byte) ([]byte, error) {

View file

@ -19,25 +19,31 @@ const (
const ( const (
X509RootDefault shared.CryptoSystemRootId = 0 X509RootDefault shared.CryptoSystemRootId = 0
X509RootClass3 shared.CryptoSystemRootId = 1 X509RootClass3 shared.CryptoSystemRootId = 1
X509RootClass3s shared.CryptoSystemRootId = 2 // The following roots existed in the old server.pl but had
X509Root3 shared.CryptoSystemRootId = 3 // no profile configurations and were thus unusable
X509Root4 shared.CryptoSystemRootId = 4 //
X509Root5 shared.CryptoSystemRootId = 5 // X509RootClass3s shared.CryptoSystemRootId = 2
// X509Root3 shared.CryptoSystemRootId = 3
// X509Root4 shared.CryptoSystemRootId = 4
// X509Root5 shared.CryptoSystemRootId = 5
) )
const ( const (
X509ProfileClient shared.CertificateProfileId = 0 X509ProfileClient shared.CertificateProfileId = 0
X509ProfileClientOrg shared.CertificateProfileId = 1 X509ProfileClientOrg shared.CertificateProfileId = 1
X509ProfileClientCodesign shared.CertificateProfileId = 2 X509ProfileClientCodesign shared.CertificateProfileId = 2
X509ProfileClientMachine shared.CertificateProfileId = 3
X509ProfileClientAds shared.CertificateProfileId = 4
X509ProfileServer shared.CertificateProfileId = 5 X509ProfileServer shared.CertificateProfileId = 5
X509ProfileServerOrg shared.CertificateProfileId = 6 X509ProfileServerOrg shared.CertificateProfileId = 6
X509ProfileServerJabber shared.CertificateProfileId = 7
X509ProfileOCSP shared.CertificateProfileId = 8 X509ProfileOCSP shared.CertificateProfileId = 8
X509ProfileTimestamp shared.CertificateProfileId = 9 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 ( const (
@ -65,30 +71,172 @@ const (
func NewCommandProcessor() *CommandProcessor { func NewCommandProcessor() *CommandProcessor {
settings := NewCommandProcessorSettings() 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{ cryptoSystems := map[shared.CryptoSystemId]*CryptoSystem{
CsX509: { CsX509: {
Name: "X.509", Name: "X.509",
Roots: map[shared.CryptoSystemRootId]interface{}{ Roots: map[shared.CryptoSystemRootId]interface{}{
X509RootDefault: x509_ops.NewRoot(settings.CABaseDir, "openssl", "CA", X509RootDefault), X509RootDefault: x509_ops.NewRoot(
X509RootClass3: x509_ops.NewRoot(settings.CABaseDir, "class3", "class3", X509RootClass3), settings.CABaseDir,
X509RootClass3s: &x509_ops.Root{Name: "class3s"}, "openssl",
X509Root3: &x509_ops.Root{Name: "root3"}, "CA",
X509Root4: &x509_ops.Root{Name: "root4"}, X509RootDefault,
X509Root5: &x509_ops.Root{Name: "root5"}, // 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{}{ Profiles: map[shared.CertificateProfileId]interface{}{
X509ProfileClient: &x509_ops.Profile{Name: "client"}, X509ProfileClient: x509_ops.NewProfile(
X509ProfileClientOrg: &x509_ops.Profile{Name: "client-org"}, "client",
X509ProfileClientCodesign: &x509_ops.Profile{Name: "client-codesign"}, clientPrototype,
X509ProfileClientMachine: &x509_ops.Profile{Name: "client-machine"}, []x509_ops.SubjectDnField{
X509ProfileClientAds: &x509_ops.Profile{Name: "client-ads"}, x509_ops.SubjectDnFieldCommonName,
X509ProfileServer: &x509_ops.Profile{Name: "server"}, x509_ops.SubjectDnFieldEmailAddress,
X509ProfileServerOrg: &x509_ops.Profile{Name: "server-org"}, },
X509ProfileServerJabber: &x509_ops.Profile{Name: "server-jabber"}, nil,
X509ProfileOCSP: &x509_ops.Profile{Name: "ocsp"}, true,
X509ProfileTimestamp: &x509_ops.Profile{Name: "timestamp"}, ),
X509ProfileProxy: &x509_ops.Profile{Name: "proxy"}, X509ProfileClientOrg: x509_ops.NewProfile("client-org", clientPrototype,
X509ProfileSubCA: &x509_ops.Profile{Name: "subca"}, []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 // constants for openssl invocations. Should be replaced with
// something more useful // something more useful

View file

@ -2,15 +2,18 @@ package x509_ops
import ( import (
"bufio" "bufio"
"bytes"
"crypto" "crypto"
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/sha1" "crypto/sha1"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/base64"
"encoding/hex" "encoding/hex"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"math/big" "math/big"
"os" "os"
@ -20,6 +23,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/longsleep/pkac"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"git.cacert.org/cacert-gosigner/shared" "git.cacert.org/cacert-gosigner/shared"
@ -28,26 +32,28 @@ import (
const crlLifetime = time.Hour * 24 * 7 const crlLifetime = time.Hour * 24 * 7
var (
oidPkcs9EmailAddress = []int{1, 2, 840, 113549, 1, 9, 1}
)
type Root struct { type Root struct {
Name string Name string
privateKeyFile string privateKey crypto.Signer
certificateFile string certificate *x509.Certificate
databaseFile string databaseFile string
serialNumberFile string
crlNumberFile string crlNumberFile string
crlFileName string crlFileName string
crlHashDir string crlHashDir string
crlDistributionPoints []string
ocspServers []string
} }
func (x *Root) String() string { func (x *Root) String() string {
return x.Name return x.Name
} }
type Profile struct { func loadCertificate(certificateFile string) (*x509.Certificate, error) {
Name string
}
func (x *Root) loadCertificate() (*x509.Certificate, error) {
certificateFile := x.certificateFile
pemBytes, err := ioutil.ReadFile(certificateFile) pemBytes, err := ioutil.ReadFile(certificateFile)
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, fmt.Errorf(
@ -75,21 +81,23 @@ func (x *Root) loadCertificate() (*x509.Certificate, error) {
return certificate, nil return certificate, nil
} }
func (x *Root) getPrivateKey() (crypto.Signer, error) { func loadPrivateKey(filename string) (crypto.Signer, error) {
privateKeyFile := x.privateKeyFile pemBytes, err := ioutil.ReadFile(filename)
pemBytes, err := ioutil.ReadFile(privateKeyFile)
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"could not load private key %s: %v", "could not load private key %s: %v",
privateKeyFile, filename,
err, err,
) )
} }
pemBlock, _ := pem.Decode(pemBytes) pemBlock, _ := pem.Decode(pemBytes)
if pemBlock == nil {
return nil, fmt.Errorf("no PEM data found in %s", filename)
}
if pemBlock.Type != "PRIVATE KEY" { if pemBlock.Type != "PRIVATE KEY" {
log.Warnf( log.Warnf(
"PEM in %s is probably not a private key. PEM block has type %s", "PEM in %s is probably not a private key. PEM block has type %s",
privateKeyFile, filename,
pemBlock.Type, pemBlock.Type,
) )
} }
@ -97,27 +105,46 @@ func (x *Root) getPrivateKey() (crypto.Signer, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"could no parse private key from %s: %v", "could no parse private key from %s: %v",
privateKeyFile, filename,
err, err,
) )
} }
return privateKey.(*rsa.PrivateKey), nil return privateKey.(*rsa.PrivateKey), nil
} }
func (x *Root) getNextCRLNumber() (*big.Int, error) { func (x *Root) getNextSerialNumber() (*big.Int, error) {
crlNumberFile := x.crlNumberFile // TODO: decide whether we should use 64 bit random serial numbers as
_, err := os.Stat(crlNumberFile) // recommended by CAB forum baseline requirements
serialNumberFile := x.serialNumberFile
_, err := os.Stat(serialNumberFile)
if err != nil { 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 return big.NewInt(1), nil
} }
data, err := ioutil.ReadFile(crlNumberFile) data, err := ioutil.ReadFile(x.serialNumberFile)
if err != nil { 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) result, err := common.StringAsBigInt(data)
if err != nil { 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 return result, nil
} }
@ -148,6 +175,33 @@ func (x *Root) bumpCRLNumber(current *big.Int) error {
return nil 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) { func (x *Root) loadRevokedCertificatesFromDatabase() ([]pkix.RevokedCertificate, error) {
databaseFile := x.databaseFile databaseFile := x.databaseFile
_, err := os.Stat(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) { func (x *Root) recordRevocation(certificate *x509.Certificate) (*pkix.RevokedCertificate, error) {
databaseFile := x.databaseFile _, err := os.Stat(x.databaseFile)
_, err := os.Stat(databaseFile)
if err != nil { 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 { 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() }() 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() }() defer func() { _ = outFile.Close() }()
scanner := bufio.NewScanner(inFile) 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) return nil, fmt.Errorf("could not close temporary file %s: %v", outFile.Name(), err)
} }
if err = inFile.Close(); err != nil { 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 { 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", databaseFile, databaseFile, err) 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 { 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(), databaseFile, err) return nil, fmt.Errorf("could not rename temporary file %s to %s: %v", outFile.Name(), x.databaseFile, err)
} }
if !found { 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) { 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() certificatesToRevoke, err := x.loadRevokedCertificatesFromDatabase()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -302,8 +347,8 @@ func (x *Root) GenerateCrl(algorithm x509.SignatureAlgorithm) ([]byte, *[20]byte
crlBytes, err := x509.CreateRevocationList( crlBytes, err := x509.CreateRevocationList(
rand.Reader, rand.Reader,
crlTemplate, crlTemplate,
caCertificate, x.certificate,
caPrivateKey, x.privateKey,
) )
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("could not create new CRL: %v", err) 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() { func (x *Root) checkPreconditions() {
results := []bool{ results := []bool{
x.checkFile(x.privateKeyFile, "private key"),
x.checkFile(x.certificateFile, "certificate"),
x.checkFile(x.databaseFile, "database"), x.checkFile(x.databaseFile, "database"),
x.checkFile(x.serialNumberFile, "serial number"),
x.checkFile(x.crlNumberFile, "CRL 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"), x.checkDir(x.crlHashDir, "directory for hash indexed CRLs"),
} }
for _, success := range results { for _, success := range results {
@ -399,12 +443,250 @@ func (x *Root) checkDir(path, prefix string) bool {
return ok 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{ root := &Root{
Name: name, Name: name,
privateKeyFile: path.Join(basedir, subdir, "private", "ca.key.pem"), privateKey: key,
certificateFile: path.Join(basedir, subdir, "ca.crt.pem"), certificate: cert,
databaseFile: path.Join(basedir, subdir, "index.txt"), databaseFile: path.Join(basedir, subdir, "index.txt"),
serialNumberFile: path.Join(basedir, subdir, "serial"),
crlNumberFile: path.Join(basedir, subdir, "crlnumber"), crlNumberFile: path.Join(basedir, subdir, "crlnumber"),
crlFileName: fmt.Sprintf("revoke-root%d.crl", id), crlFileName: fmt.Sprintf("revoke-root%d.crl", id),
crlHashDir: path.Join( crlHashDir: path.Join(
@ -412,7 +694,167 @@ func NewRoot(basedir, name, subdir string, id shared.CryptoSystemRootId) *Root {
"currentcrls", "currentcrls",
fmt.Sprintf("%d", id), fmt.Sprintf("%d", id),
), ),
crlDistributionPoints: crlDistributionPoints,
ocspServers: ocspServers,
} }
root.checkPreconditions() root.checkPreconditions()
return root 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,
}
}

View file

@ -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="