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