Do work to handle all kinds of encrypted TLS keys.
diff --git a/pem.go b/pem.go
new file mode 100644
index 0000000..b021eff
--- /dev/null
+++ b/pem.go
@@ -0,0 +1,56 @@
+package goscaffold
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "io/ioutil"
+)
+
+func decodePEM(fileName string, pwFunc func() []byte) ([]byte, string, error) {
+ pb, err := ioutil.ReadFile(fileName)
+ if err != nil {
+ return nil, "", err
+ }
+ block, _ := pem.Decode(pb)
+ if x509.IsEncryptedPEMBlock(block) {
+ if pwFunc == nil {
+ return nil, "", errors.New("Cannot read encrypted file without password")
+ }
+ dec, err := x509.DecryptPEMBlock(block, pwFunc())
+ if err != nil {
+ return nil, "", err
+ }
+ return dec, block.Type, nil
+ }
+ return block.Bytes, block.Type, nil
+}
+
+func getCertificate(certFile, keyFile string, pwFunc func() []byte) (tls.Certificate, error) {
+ ret := tls.Certificate{}
+
+ certBytes, _, err := decodePEM(certFile, nil)
+ if err != nil {
+ return ret, err
+ }
+ ret.Certificate = [][]byte{certBytes}
+
+ keyBytes, keyType, err := decodePEM(keyFile, pwFunc)
+ if err != nil {
+ return ret, err
+ }
+ switch keyType {
+ case "RSA PRIVATE KEY":
+ pk, err := x509.ParsePKCS1PrivateKey(keyBytes)
+ if err != nil {
+ return ret, err
+ }
+ ret.PrivateKey = pk
+ default:
+ return ret, fmt.Errorf("Invalid private key type %s", keyType)
+ }
+
+ return ret, nil
+}
diff --git a/scaffold.go b/scaffold.go
index cc64cc1..de6e7f3 100644
--- a/scaffold.go
+++ b/scaffold.go
@@ -1,6 +1,7 @@
package goscaffold
import (
+ "crypto/tls"
"errors"
"fmt"
"net"
@@ -79,10 +80,12 @@
*/
type HTTPScaffold struct {
insecurePort int
+ securePort int
managementPort int
open bool
tracker *requestTracker
insecureListener net.Listener
+ secureListener net.Listener
managementListener net.Listener
healthCheck HealthChecker
healthPath string
@@ -90,6 +93,9 @@
markdownPath string
markdownMethod string
markdownHandler MarkdownHandler
+ keyFile string
+ keyPassFunc func() []byte
+ certFile string
}
/*
@@ -99,6 +105,7 @@
func CreateHTTPScaffold() *HTTPScaffold {
return &HTTPScaffold{
insecurePort: 0,
+ securePort: -1,
managementPort: -1,
open: false,
}
@@ -114,6 +121,17 @@
}
/*
+SetSecurePort sets the port number to listen on in HTTPS mode.
+It may be set to zero, which indicates to listen on an ephemeral port.
+It must be called before Listen. It is an error to call
+Listen if this port is set and if the key and secret files are not also
+set.
+*/
+func (s *HTTPScaffold) SetSecurePort(ip int) {
+ s.securePort = ip
+}
+
+/*
InsecureAddress returns the actual address (including the port if an
ephemeral port was used) where we are listening. It must only be
called after "Listen."
@@ -126,6 +144,18 @@
}
/*
+SecureAddress returns the actual address (including the port if an
+ephemeral port was used) where we are listening on HTTPS. It must only be
+called after "Listen."
+*/
+func (s *HTTPScaffold) SecureAddress() string {
+ if s.secureListener == nil {
+ return ""
+ }
+ return s.secureListener.Addr().String()
+}
+
+/*
SetManagementPort sets the port number for management operations, including
health checks and diagnostic operations. If not set, then these operations
happen on the other ports. If set, then they only happen on this port.
@@ -147,6 +177,25 @@
}
/*
+SetCertFile sets the name of the file that the server will read to get its
+own TLS certificate. It is only consulted if "securePort" is >= 0.
+*/
+func (s *HTTPScaffold) SetCertFile(fn string) {
+ s.certFile = fn
+}
+
+/*
+SetKeyFile sets the name of the file that the server will read to get its
+own TLS key. It is only consulted if "securePort" is >= 0.
+If "getPass" is non-null, then the function will be called at startup time
+to retrieve the password for the key file.
+*/
+func (s *HTTPScaffold) SetKeyFile(fn string, getPass func() []byte) {
+ s.keyFile = fn
+ s.keyPassFunc = getPass
+}
+
+/*
SetHealthPath sets up a health check on the management port (if set) or
otherwise the main port. If a health check function has been supplied,
it will return 503 if the function returns "Failed" and 200 otherwise.
@@ -210,18 +259,45 @@
func (s *HTTPScaffold) Open() error {
s.tracker = startRequestTracker(DefaultGraceTimeout)
- il, err := net.ListenTCP("tcp", &net.TCPAddr{
- Port: s.insecurePort,
- })
- if err != nil {
- return err
- }
- s.insecureListener = il
- defer func() {
- if !s.open {
- il.Close()
+ if s.insecurePort >= 0 {
+ il, err := net.ListenTCP("tcp", &net.TCPAddr{
+ Port: s.insecurePort,
+ })
+ if err != nil {
+ return err
}
- }()
+ s.insecureListener = il
+ defer func() {
+ if !s.open {
+ il.Close()
+ }
+ }()
+ }
+
+ if s.securePort >= 0 {
+ if s.keyFile == "" || s.certFile == "" {
+ return errors.New("key and certificate files must be set")
+ }
+ cert, err := getCertificate(s.certFile, s.keyFile, s.keyPassFunc)
+ if err != nil {
+ return err
+ }
+ tlsConfig := &tls.Config{
+ Certificates: []tls.Certificate{cert},
+ }
+ sl, err := net.ListenTCP("tcp", &net.TCPAddr{
+ Port: s.securePort,
+ })
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if !s.open {
+ sl.Close()
+ }
+ }()
+ s.secureListener = tls.NewListener(sl, tlsConfig)
+ }
if s.managementPort >= 0 {
ml, err := net.ListenTCP("tcp", &net.TCPAddr{
@@ -286,11 +362,21 @@
mainHandler = mgmtHandler
}
- go http.Serve(s.insecureListener, mainHandler)
+ if s.insecureListener != nil {
+ go http.Serve(s.insecureListener, mainHandler)
+ }
+ if s.secureListener != nil {
+ go http.Serve(s.secureListener, mainHandler)
+ }
err := <-s.tracker.C
- s.insecureListener.Close()
+ if s.insecureListener != nil {
+ s.insecureListener.Close()
+ }
+ if s.secureListener != nil {
+ s.secureListener.Close()
+ }
if s.managementListener != nil {
s.managementListener.Close()
}
diff --git a/scaffold_test.go b/scaffold_test.go
index 2321ad8..361a6f0 100644
--- a/scaffold_test.go
+++ b/scaffold_test.go
@@ -1,6 +1,7 @@
package goscaffold
import (
+ "crypto/tls"
"encoding/json"
"errors"
"fmt"
@@ -14,6 +15,14 @@
. "github.com/onsi/gomega"
)
+var insecureClient = &http.Client{
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: true,
+ },
+ },
+}
+
var _ = Describe("Scaffold Tests", func() {
It("Validate framework", func() {
s := CreateHTTPScaffold()
@@ -266,6 +275,120 @@
s.Shutdown(nil)
Eventually(stopChan).Should(Receive(Equal(ErrManualStop)))
})
+
+ It("Secure And Insecure Ports", func() {
+ s := CreateHTTPScaffold()
+ s.SetSecurePort(0)
+ s.SetKeyFile("./testkeys/clearkey.pem", nil)
+ s.SetCertFile("./testkeys/clearcert.pem")
+ stopChan := make(chan error)
+ err := s.Open()
+ Expect(err).Should(Succeed())
+
+ go func() {
+ fmt.Fprintf(GinkgoWriter, "Gonna listen on %s and %s\n",
+ s.InsecureAddress(), s.SecureAddress())
+ stopErr := s.Listen(&testHandler{})
+ fmt.Fprintf(GinkgoWriter, "Done listening\n")
+ stopChan <- stopErr
+ }()
+
+ Eventually(func() bool {
+ return testGet(s, "")
+ }, 5*time.Second).Should(BeTrue())
+ Eventually(func() bool {
+ return testGetSecure(s, "")
+ }, time.Second).Should(BeTrue())
+
+ shutdownErr := errors.New("Validate")
+ s.Shutdown(shutdownErr)
+ Eventually(stopChan).Should(Receive(Equal(shutdownErr)))
+ })
+
+ It("Secure Port Only", func() {
+ s := CreateHTTPScaffold()
+ s.SetSecurePort(0)
+ s.SetInsecurePort(-1)
+ s.SetKeyFile("./testkeys/clearkey.pem", nil)
+ s.SetCertFile("./testkeys/clearcert.pem")
+ stopChan := make(chan error)
+ err := s.Open()
+ Expect(err).Should(Succeed())
+ Expect(s.InsecureAddress()).Should(BeEmpty())
+
+ go func() {
+ fmt.Fprintf(GinkgoWriter, "Gonna listen on %s\n",
+ s.SecureAddress())
+ stopErr := s.Listen(&testHandler{})
+ fmt.Fprintf(GinkgoWriter, "Done listening\n")
+ stopChan <- stopErr
+ }()
+
+ Eventually(func() bool {
+ return testGetSecure(s, "")
+ }, 5*time.Second).Should(BeTrue())
+
+ shutdownErr := errors.New("Validate")
+ s.Shutdown(shutdownErr)
+ Eventually(stopChan).Should(Receive(Equal(shutdownErr)))
+ })
+
+ It("Secure Port Encrypted Key", func() {
+ s := CreateHTTPScaffold()
+ s.SetSecurePort(0)
+ s.SetInsecurePort(-1)
+ s.SetKeyFile("./testkeys/serverkey.pem", func() []byte {
+ return []byte("secure")
+ })
+ s.SetCertFile("./testkeys/servercert.pem")
+ stopChan := make(chan error)
+ err := s.Open()
+ Expect(err).Should(Succeed())
+ Expect(s.InsecureAddress()).Should(BeEmpty())
+
+ go func() {
+ fmt.Fprintf(GinkgoWriter, "Gonna listen on %s\n",
+ s.SecureAddress())
+ stopErr := s.Listen(&testHandler{})
+ fmt.Fprintf(GinkgoWriter, "Done listening\n")
+ stopChan <- stopErr
+ }()
+
+ Eventually(func() bool {
+ return testGetSecure(s, "")
+ }, 5*time.Second).Should(BeTrue())
+
+ shutdownErr := errors.New("Validate")
+ s.Shutdown(shutdownErr)
+ Eventually(stopChan).Should(Receive(Equal(shutdownErr)))
+ })
+
+ It("Read PEM files", func() {
+ _, t, err := decodePEM("./testkeys/clearkey.pem", nil)
+ Expect(err).Should(Succeed())
+ Expect(t).Should(Equal("RSA PRIVATE KEY"))
+ _, t, err = decodePEM("./testkeys/clearcert.pem", nil)
+ Expect(err).Should(Succeed())
+ Expect(t).Should(Equal("CERTIFICATE"))
+ _, err = getCertificate("./testkeys/clearcert.pem", "./testkeys/clearkey.pem", nil)
+ Expect(err).Should(Succeed())
+
+ _, _, err = decodePEM("./testkeys/servercert.pem", nil)
+ Expect(err).Should(Succeed())
+ _, _, err = decodePEM("./testkeys/serverkey.pem", nil)
+ Expect(err).ShouldNot(Succeed())
+ _, _, err = decodePEM("./testkeys/serverkey.pem", func() []byte {
+ return []byte("notsecure")
+ })
+ Expect(err).ShouldNot(Succeed())
+ _, _, err = decodePEM("./testkeys/serverkey.pem", func() []byte {
+ return []byte("secure")
+ })
+ Expect(err).Should(Succeed())
+ _, err = getCertificate("./testkeys/servercert.pem", "./testkeys/serverkey.pem", func() []byte {
+ return []byte("notsecure")
+ })
+ })
})
func getText(url string) (int, string) {
@@ -309,6 +432,20 @@
return true
}
+func testGetSecure(s *HTTPScaffold, path string) bool {
+ resp, err := insecureClient.Get(fmt.Sprintf("https://%s", s.SecureAddress()))
+ if err != nil {
+ fmt.Fprintf(GinkgoWriter, "Get %s = %s\n", path, err)
+ return false
+ }
+ resp.Body.Close()
+ if resp.StatusCode != 200 {
+ fmt.Fprintf(GinkgoWriter, "Get %s = %d\n", path, resp.StatusCode)
+ return false
+ }
+ return true
+}
+
type testHandler struct {
}
diff --git a/testkeys/ca.conf b/testkeys/ca.conf
new file mode 100644
index 0000000..39f67c2
--- /dev/null
+++ b/testkeys/ca.conf
@@ -0,0 +1,132 @@
+# OpenSSL root CA configuration file.
+# Copy to `/root/ca/openssl.cnf`.
+
+[ ca ]
+# `man ca`
+default_ca = CA_default
+
+[ CA_default ]
+# Directory and file locations.
+dir = .
+certs = $dir
+crl_dir = $dir
+new_certs_dir = $dir
+database = $dir/index.txt
+serial = $dir/serial
+RANDFILE = $dir/private/.rand
+
+# The root key and root certificate.
+private_key = $dir/cakey.pem
+certificate = $dir/cacert.pem
+
+# For certificate revocation lists.
+crlnumber = $dir/crlnumber
+crl = $dir/cacrl.pem
+crl_extensions = crl_ext
+default_crl_days = 30
+
+# SHA-1 is deprecated, so use SHA-2 instead.
+default_md = sha256
+
+name_opt = ca_default
+cert_opt = ca_default
+default_days = 375
+preserve = no
+policy = policy_strict
+
+[ policy_strict ]
+# The root CA should only sign intermediate certificates that match.
+# See the POLICY FORMAT section of `man ca`.
+countryName = match
+stateOrProvinceName = match
+organizationName = match
+organizationalUnitName = optional
+commonName = supplied
+emailAddress = optional
+
+[ policy_loose ]
+# Allow the intermediate CA to sign a more diverse range of certificates.
+# See the POLICY FORMAT section of the `ca` man page.
+countryName = optional
+stateOrProvinceName = optional
+localityName = optional
+organizationName = optional
+organizationalUnitName = optional
+commonName = supplied
+emailAddress = optional
+
+[ req ]
+# Options for the `req` tool (`man req`).
+default_bits = 2048
+distinguished_name = req_distinguished_name
+string_mask = utf8only
+
+# SHA-1 is deprecated, so use SHA-2 instead.
+default_md = sha256
+
+# Extension to add when the -x509 option is used.
+x509_extensions = v3_ca
+
+[ req_distinguished_name ]
+# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
+countryName = Country Name (2 letter code)
+stateOrProvinceName = State or Province Name
+localityName = Locality Name
+0.organizationName = Organization Name
+organizationalUnitName = Organizational Unit Name
+commonName = Common Name
+emailAddress = Email Address
+
+# Optionally, specify some defaults.
+countryName_default = GB
+stateOrProvinceName_default = England
+localityName_default =
+0.organizationName_default = Alice Ltd
+organizationalUnitName_default =
+emailAddress_default =
+
+[ v3_ca ]
+# Extensions for a typical CA (`man x509v3_config`).
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer
+basicConstraints = critical, CA:true
+keyUsage = critical, digitalSignature, cRLSign, keyCertSign
+
+[ v3_intermediate_ca ]
+# Extensions for a typical intermediate CA (`man x509v3_config`).
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer
+basicConstraints = critical, CA:true, pathlen:0
+keyUsage = critical, digitalSignature, cRLSign, keyCertSign
+
+[ usr_cert ]
+# Extensions for client certificates (`man x509v3_config`).
+basicConstraints = CA:FALSE
+nsCertType = client, email
+nsComment = "OpenSSL Generated Client Certificate"
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid,issuer
+keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
+extendedKeyUsage = clientAuth, emailProtection
+
+[ server_cert ]
+# Extensions for server certificates (`man x509v3_config`).
+basicConstraints = CA:FALSE
+nsCertType = server
+nsComment = "OpenSSL Generated Server Certificate"
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid,issuer:always
+keyUsage = critical, digitalSignature, keyEncipherment
+extendedKeyUsage = serverAuth
+
+[ crl_ext ]
+# Extension for CRLs (`man x509v3_config`).
+authorityKeyIdentifier=keyid:always
+
+[ ocsp ]
+# Extension for OCSP signing certificates (`man ocsp`).
+basicConstraints = CA:FALSE
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid,issuer
+keyUsage = critical, digitalSignature
+extendedKeyUsage = critical, OCSPSigning
diff --git a/testkeys/cacert.pem b/testkeys/cacert.pem
new file mode 100644
index 0000000..181a85f
--- /dev/null
+++ b/testkeys/cacert.pem
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFuDCCA6CgAwIBAgIJAPBP+fzh8s9AMA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNV
+BAYTAlVTMQswCQYDVQQIDAJDQTERMA8GA1UEBwwIU2FuIEpvc2UxDzANBgNVBAoM
+BkFwaWdlZTEUMBIGA1UECwwLRW5naW5lZXJpbmcxEzARBgNVBAMMCnNjYWZmb2xk
+Y2EwHhcNMTYxMTEwMTkzODUyWhcNNDQwMzI3MTkzODUyWjBpMQswCQYDVQQGEwJV
+UzELMAkGA1UECAwCQ0ExETAPBgNVBAcMCFNhbiBKb3NlMQ8wDQYDVQQKDAZBcGln
+ZWUxFDASBgNVBAsMC0VuZ2luZWVyaW5nMRMwEQYDVQQDDApzY2FmZm9sZGNhMIIC
+IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqiGUrzIu5ZWBqdgDqzKa8B0r
+bT3rQMhj1khljXoDBPiYa74njJJzG/+OLdSSeacIkzBZmVmrQgT/gjxlk7x3p6We
+QC+AbHmc2rqkjV+uKPCiJEwumFaNjWe8u6B1pJ/g125jo5k8BTQs48a8GJAjxU2O
+l1rpLTSq6YQfMq7oHoG/wACzNouPTem+SuQkhMQR0EdMX8Fkx1oZoG4bcK3sApQv
+zeFntyBH9Q6Pm7zTHgxB1fFdYGeFsVItAGY61UA+DhEy9vspQwkEJT2Av/EzjTJK
+ctEL32uNdNZGuPQvjE7qE0oVUsIJkkxcWtXQHnPlhdxbYtjQbQZqVHuFtkT2PQIq
+m+hDpqJwGo3vIge1KT3uvAO93JpH3ThQZzWw0Zl0Vbge2Tr+/y+sHXD90mbA0Qvh
+tMB8iWwSJ1Pi06zjG5FXRM3E6Z5/2lDYTn4d/UCkgMc9qztNFHfkywa5s+5/J9lf
+Xgr6IZoSPkyoef0WPU+p8rQw4pqi/obZmXSAhQ4azCxwOL6QNwYEFHtoDSIduGCQ
+8RIxCJz9ZWnB6Vg9bgKQ9EWEJksWcFwHNzQESi8HOVL+h9oUrYl2cKAoZZGgtPLI
+DpxXsZI9HGIojjo14IaeW+iBHDJGzgm0wzYIU/JqT06QVyB3rOZ/8hlmM17Wbqg+
+kawIEX5uzIQBl6itm60CAwEAAaNjMGEwHQYDVR0OBBYEFEDn0aSk/LQmsCTcTaB/
+TM6xU6wrMB8GA1UdIwQYMBaAFEDn0aSk/LQmsCTcTaB/TM6xU6wrMA8GA1UdEwEB
+/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQAqnp1M
+ZeqGSiUrUYH4DegSdDUVnevRkCSRhIPIvESPQQFTMRi6AlJhJ8+mD1+sY6Hmc1/H
+bNHBeuEFsHzOWWP7Qe6jzTcCE97tIjIcK2Iu4FBAubN+fvTtLLoBq9KRE82Rw0qZ
+c//oswJQm6x/0no6V8mKjl8CjrVuBfkhS4yDHcsUDzOVlXjr6VhUb8Y19AIaINDa
+VveoOR4sXVjGow74+ztChguX/baxgGVoy85p/epTOYlh+HGHdr4uLuQtJVCgccQj
+NUnCrCzx2qC+25anwNv9OEvH9Gx7SZemfdrC27I44P9YVgHPqgC7/Ly589yr5Qq3
+ECcUza7sWYoAAdE1oY7jpdElueb0cN59OvIsAXNWSOw5Q0ZDYoECzwUJve3IOoYp
+maEdWXRDs9nvGnyi9c5Y4qmJN/poy+Vsk2XJarsYJyefRxYRI3kMO7wI7cOMukA2
+m1k1gP4l1XCoCn1AB9sUD6/pl85sy2q8OtO/B7C2WgDEuZpYBncOs8e3xtaxTxnc
+rwvzKtc0JxFoYgvSsZjUMwQIoCG9SkiCBpJbZQQd6yisCk5jWY3HshEamVOZKby8
+ISKgleoAxJ2+S0mG4d0KZ1NHiksdDWXT8Ls+CwntZO29AA7GDAwVGX7pS3frMHZq
+24V2zNS++3l9+tOMIO84r9oSMzzrwo+ky0aNgg==
+-----END CERTIFICATE-----
diff --git a/testkeys/cakey.pem b/testkeys/cakey.pem
new file mode 100644
index 0000000..c47b9a3
--- /dev/null
+++ b/testkeys/cakey.pem
@@ -0,0 +1,54 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-256-CBC,5A8D3DB61B4F5C18ACE9D211ABA149B7
+
+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
+-----END RSA PRIVATE KEY-----
diff --git a/testkeys/clearcert.pem b/testkeys/clearcert.pem
new file mode 100644
index 0000000..82d6a40
--- /dev/null
+++ b/testkeys/clearcert.pem
@@ -0,0 +1,113 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number: 4096 (0x1000)
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: C=US, ST=CA, L=San Jose, O=Apigee, OU=Engineering, CN=scaffoldca
+ Validity
+ Not Before: Nov 10 19:41:12 2016 GMT
+ Not After : Mar 27 19:41:12 2044 GMT
+ Subject: C=US, ST=CA, O=Apigee, OU=Engineering, CN=clearserver
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public Key: (2048 bit)
+ Modulus (2048 bit):
+ 00:be:8c:c2:24:fe:1f:30:96:62:98:73:b0:00:7c:
+ 70:06:33:f9:84:02:7a:b2:53:e4:c3:29:9c:b9:60:
+ 57:52:a6:2e:bd:9c:6c:a0:60:0a:fa:06:2b:cc:17:
+ 84:98:4c:c6:b8:9e:19:11:e9:08:97:58:6d:41:63:
+ 9c:ad:1c:22:3b:d6:a5:a9:7a:5d:00:e7:da:ce:ae:
+ 6d:59:32:94:f5:2e:d5:68:aa:9e:30:55:1f:98:8f:
+ b8:1c:b0:6b:cf:a4:cc:f2:3e:56:2e:bb:ed:77:d5:
+ e0:01:65:92:f4:c9:cc:fe:0f:19:ae:5d:cd:8c:46:
+ 36:8b:7f:51:0d:19:8f:f6:9b:fb:c5:c9:dc:1c:09:
+ ec:b7:e2:a3:1c:f8:84:bd:ba:2c:7e:ff:f2:13:ad:
+ 0d:7e:8a:4f:f7:0a:cb:6b:71:85:e3:2a:d5:4f:b4:
+ 4e:1f:f4:27:7b:4f:6c:e1:2b:42:a7:04:d1:d5:37:
+ 8f:c5:a1:b3:05:af:ba:08:c1:29:c2:f2:45:ee:0a:
+ ac:9e:61:e6:be:72:ce:1e:5c:54:75:b8:6d:a3:4a:
+ 8e:45:a7:86:b4:01:48:e0:00:ff:cf:76:74:d7:34:
+ 72:cb:a0:e0:17:10:c7:7d:0a:60:ba:2a:10:80:21:
+ e9:35:82:79:74:01:57:fb:c0:6a:34:ae:2f:19:fa:
+ 1a:5b
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Basic Constraints:
+ CA:FALSE
+ Netscape Cert Type:
+ SSL Server
+ Netscape Comment:
+ OpenSSL Generated Server Certificate
+ X509v3 Subject Key Identifier:
+ 5B:26:4E:31:7B:83:7A:13:45:F7:D1:AE:BC:E1:7D:16:B9:4B:28:2C
+ X509v3 Authority Key Identifier:
+ keyid:40:E7:D1:A4:A4:FC:B4:26:B0:24:DC:4D:A0:7F:4C:CE:B1:53:AC:2B
+ DirName:/C=US/ST=CA/L=San Jose/O=Apigee/OU=Engineering/CN=scaffoldca
+ serial:F0:4F:F9:FC:E1:F2:CF:40
+
+ X509v3 Key Usage: critical
+ Digital Signature, Key Encipherment
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication
+ Signature Algorithm: sha256WithRSAEncryption
+ 1f:74:6d:85:f5:4e:49:0b:98:da:fd:15:40:4f:9d:fe:2d:ea:
+ 01:96:d9:77:cd:10:eb:99:3f:29:57:2d:c7:bf:01:1c:96:ad:
+ 1a:3c:54:f0:4d:c4:8b:6d:8d:b8:36:12:f2:08:59:d8:01:5d:
+ c8:a9:66:c1:fd:cf:ca:cd:9f:cb:5f:ac:23:c3:ef:e5:f6:b9:
+ da:4c:f6:56:fa:6b:9f:93:55:bd:f9:7f:d3:73:96:5d:d0:e6:
+ be:07:52:5a:b7:1f:ff:17:1b:38:ac:8b:9f:84:d1:d6:5d:a2:
+ 9d:fe:04:ac:77:69:77:55:bc:3c:2d:0d:1c:e5:cf:84:00:7b:
+ 7d:01:04:d8:1f:db:fc:ba:b7:e9:e5:17:08:6d:2c:49:4b:0c:
+ 08:4f:b4:7c:df:69:ed:6c:75:2f:76:28:e0:80:51:03:69:77:
+ 67:a5:e3:51:01:b6:3b:df:98:aa:e5:49:2e:1c:6e:d7:c9:fb:
+ f9:be:5d:55:63:9c:69:0d:90:90:40:50:2a:77:c3:07:ea:11:
+ b1:23:3a:9c:d9:5e:ad:f2:12:fe:e6:37:d3:6f:00:1a:bf:5a:
+ aa:d9:8c:d3:17:43:0f:e5:18:0b:7e:09:41:10:9a:64:c4:ad:
+ 4d:c9:b8:81:df:b9:f8:e2:36:10:80:fc:0a:89:e6:0a:3e:cb:
+ fb:a6:a5:44:c3:b3:db:6a:b2:62:f7:a6:ce:6a:2a:64:9a:7b:
+ 32:ef:33:74:f1:cf:0b:57:50:8d:13:64:bd:26:f9:71:e4:7f:
+ ad:a7:9f:b6:3e:b3:a8:42:2b:2e:cb:75:09:4a:ce:ee:ff:04:
+ d9:42:4a:cb:0a:8b:82:d0:b2:d9:99:fa:8e:ae:73:cf:fe:8d:
+ 6a:23:2d:ea:23:ea:c7:b5:d4:81:b8:c5:ac:3a:77:03:53:7c:
+ f2:49:f8:46:58:f6:0b:16:09:54:75:5e:0e:3a:96:32:be:8c:
+ 0e:02:aa:69:34:52:05:0d:d8:6a:67:15:bc:6a:67:07:1d:a5:
+ 3c:d3:31:39:f9:73:82:d9:57:7f:f2:26:1b:cb:fe:db:db:70:
+ 89:d5:3c:c8:17:62:93:bc:08:3a:ab:2c:54:03:9f:26:6f:92:
+ 44:9f:e4:02:e2:5c:59:22:07:d2:ad:e0:1a:e3:7d:8a:d9:2a:
+ 0b:03:c6:a8:d3:20:13:1a:49:a6:33:cf:66:2e:f7:93:b1:c1:
+ 1c:ed:2c:6f:59:4a:f0:db:bc:b7:97:8d:69:1d:bf:6b:8b:0a:
+ 74:be:0b:bf:4b:bb:c3:77:5d:e0:8c:7a:1c:d1:63:6e:93:d4:
+ 9d:00:19:ef:67:f6:fb:d4:70:68:44:6f:33:7e:b9:3e:65:2f:
+ 96:eb:e6:d2:35:70:85:f5
+-----BEGIN CERTIFICATE-----
+MIIFdzCCA1+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwaTELMAkGA1UEBhMCVVMx
+CzAJBgNVBAgMAkNBMREwDwYDVQQHDAhTYW4gSm9zZTEPMA0GA1UECgwGQXBpZ2Vl
+MRQwEgYDVQQLDAtFbmdpbmVlcmluZzETMBEGA1UEAwwKc2NhZmZvbGRjYTAeFw0x
+NjExMTAxOTQxMTJaFw00NDAzMjcxOTQxMTJaMFcxCzAJBgNVBAYTAlVTMQswCQYD
+VQQIDAJDQTEPMA0GA1UECgwGQXBpZ2VlMRQwEgYDVQQLDAtFbmdpbmVlcmluZzEU
+MBIGA1UEAwwLY2xlYXJzZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQC+jMIk/h8wlmKYc7AAfHAGM/mEAnqyU+TDKZy5YFdSpi69nGygYAr6BivM
+F4SYTMa4nhkR6QiXWG1BY5ytHCI71qWpel0A59rOrm1ZMpT1LtVoqp4wVR+Yj7gc
+sGvPpMzyPlYuu+131eABZZL0ycz+DxmuXc2MRjaLf1ENGY/2m/vFydwcCey34qMc
++IS9uix+//ITrQ1+ik/3CstrcYXjKtVPtE4f9Cd7T2zhK0KnBNHVN4/FobMFr7oI
+wSnC8kXuCqyeYea+cs4eXFR1uG2jSo5Fp4a0AUjgAP/PdnTXNHLLoOAXEMd9CmC6
+KhCAIek1gnl0AVf7wGo0ri8Z+hpbAgMBAAGjggE5MIIBNTAJBgNVHRMEAjAAMBEG
+CWCGSAGG+EIBAQQEAwIGQDAzBglghkgBhvhCAQ0EJhYkT3BlblNTTCBHZW5lcmF0
+ZWQgU2VydmVyIENlcnRpZmljYXRlMB0GA1UdDgQWBBRbJk4xe4N6E0X30a684X0W
+uUsoLDCBmwYDVR0jBIGTMIGQgBRA59GkpPy0JrAk3E2gf0zOsVOsK6FtpGswaTEL
+MAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMREwDwYDVQQHDAhTYW4gSm9zZTEPMA0G
+A1UECgwGQXBpZ2VlMRQwEgYDVQQLDAtFbmdpbmVlcmluZzETMBEGA1UEAwwKc2Nh
+ZmZvbGRjYYIJAPBP+fzh8s9AMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggr
+BgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAgEAH3RthfVOSQuY2v0VQE+d/i3qAZbZ
+d80Q65k/KVctx78BHJatGjxU8E3Ei22NuDYS8ghZ2AFdyKlmwf3Pys2fy1+sI8Pv
+5fa52kz2Vvprn5NVvfl/03OWXdDmvgdSWrcf/xcbOKyLn4TR1l2inf4ErHdpd1W8
+PC0NHOXPhAB7fQEE2B/b/Lq36eUXCG0sSUsMCE+0fN9p7Wx1L3Yo4IBRA2l3Z6Xj
+UQG2O9+YquVJLhxu18n7+b5dVWOcaQ2QkEBQKnfDB+oRsSM6nNlerfIS/uY3028A
+Gr9aqtmM0xdDD+UYC34JQRCaZMStTcm4gd+5+OI2EID8ConmCj7L+6alRMOz22qy
+YvemzmoqZJp7Mu8zdPHPC1dQjRNkvSb5ceR/raeftj6zqEIrLst1CUrO7v8E2UJK
+ywqLgtCy2Zn6jq5zz/6NaiMt6iPqx7XUgbjFrDp3A1N88kn4Rlj2CxYJVHVeDjqW
+Mr6MDgKqaTRSBQ3YamcVvGpnBx2lPNMxOflzgtlXf/ImG8v+29twidU8yBdik7wI
+OqssVAOfJm+SRJ/kAuJcWSIH0q3gGuN9itkqCwPGqNMgExpJpjPPZi73k7HBHO0s
+b1lK8Nu8t5eNaR2/a4sKdL4Lv0u7w3dd4Ix6HNFjbpPUnQAZ72f2+9RwaERvM365
+PmUvluvm0jVwhfU=
+-----END CERTIFICATE-----
diff --git a/testkeys/clearcsr.pem b/testkeys/clearcsr.pem
new file mode 100644
index 0000000..fdb0bac
--- /dev/null
+++ b/testkeys/clearcsr.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICrzCCAZcCAQAwajELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMREwDwYDVQQH
+DAhTYW4gSm9zZTEPMA0GA1UECgwGQXBpZ2VlMRQwEgYDVQQLDAtFbmdpbmVlcmlu
+ZzEUMBIGA1UEAwwLY2xlYXJzZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQC+jMIk/h8wlmKYc7AAfHAGM/mEAnqyU+TDKZy5YFdSpi69nGygYAr6
+BivMF4SYTMa4nhkR6QiXWG1BY5ytHCI71qWpel0A59rOrm1ZMpT1LtVoqp4wVR+Y
+j7gcsGvPpMzyPlYuu+131eABZZL0ycz+DxmuXc2MRjaLf1ENGY/2m/vFydwcCey3
+4qMc+IS9uix+//ITrQ1+ik/3CstrcYXjKtVPtE4f9Cd7T2zhK0KnBNHVN4/FobMF
+r7oIwSnC8kXuCqyeYea+cs4eXFR1uG2jSo5Fp4a0AUjgAP/PdnTXNHLLoOAXEMd9
+CmC6KhCAIek1gnl0AVf7wGo0ri8Z+hpbAgMBAAGgADANBgkqhkiG9w0BAQsFAAOC
+AQEAa8ZaRQhC/rH/SJLR6DdCypaAtlXMn7L+TQJHlbgvyfnmNVz5kSb0UHqEEdPy
+fCNX74JERUzcSiDCzdycs38Tj4RWngdeydSX1Z72zXrQ2UU4LyWCyzyPRd5kAouD
+sI0BQX2mtZsuNbXlPR52RW08i62Ifp8NaAco7LPPF2XNXO+f/4uBxTtYaM2sjyfz
+NCi4AkCSE6mK4PHGahLWPsUuXU0uMIlVzyKVuCof6YCeVhmA3r9b/eq1damw+5uf
+qSC/115uMjvCk896of4gJXGDGhDKYIKEgZjoz/qSkCpXBdASC2iI+4L+S2xkvdFl
+ZhJM3bIWKJqhgJFHq1/nq8s78g==
+-----END CERTIFICATE REQUEST-----
diff --git a/testkeys/clearkey.pem b/testkeys/clearkey.pem
new file mode 100644
index 0000000..0f70612
--- /dev/null
+++ b/testkeys/clearkey.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAvozCJP4fMJZimHOwAHxwBjP5hAJ6slPkwymcuWBXUqYuvZxs
+oGAK+gYrzBeEmEzGuJ4ZEekIl1htQWOcrRwiO9alqXpdAOfazq5tWTKU9S7VaKqe
+MFUfmI+4HLBrz6TM8j5WLrvtd9XgAWWS9MnM/g8Zrl3NjEY2i39RDRmP9pv7xcnc
+HAnst+KjHPiEvbosfv/yE60NfopP9wrLa3GF4yrVT7ROH/Qne09s4StCpwTR1TeP
+xaGzBa+6CMEpwvJF7gqsnmHmvnLOHlxUdbhto0qORaeGtAFI4AD/z3Z01zRyy6Dg
+FxDHfQpguioQgCHpNYJ5dAFX+8BqNK4vGfoaWwIDAQABAoIBAHg9hEkpnm7fhsni
+vp4f5+TSKVJW6ivbapCIkqAbnh7GRNpbeeRdlq+c80gvUvl0ATYAdZtqrIQsRY0s
+mXJXPXb7BMCEOWnX5KflQ5U0qPBqpDoO6BFhtbVkg25/GRkHSlhWY7XL2pZy/I6F
+FQ2oxdVPnAn+oTUyHTs7lN6qUcDwJ9dwaP5GDfpEVejn2FnAp4y6ZakgNtwMLIxt
+m1JwAk//U1rquIgqS34UANOf1L5yttX8pPHUxbzqSYz2PBbdf6zZfbpGKPvJEWUi
+XngcpWIzOZ1GmPLUngJqo/Qve4guzA4deHNZOGM7exaemT/E9yHHrbBSiFTEX44A
+1GstaAECgYEA6rA8S+CbvLfwtPlrMyizv7M8ZKOr7PogeM3tjH+xerfqLhwXJsAy
+l1vffgnXcqP1LSKNm1HXe5XvNggKh381sB080u5PycyxKJegZr0q3X8pGnQ0ozR+
+wFjVqTfzkZUkzV7qvF2Sw2XB0Z1gaQp9s1RC6ZovEYHjPyBalY32DAECgYEAz9pw
+sgImuAkxAIwewh4+2pp28fCm7Z5zHB9/7uLdcvlfkk7Osr5FTqCIb036o5HuM5pY
+bD5+PuET5v8fs5jJrKMbM2fBnnO4tn4f9c1uohs5qfFtNF8rEk4tZbdAoOM/lq+j
+RjF8tIyHUg6O4lulpBsijrSlseNpEsYg1jF71lsCgYBhK60bgZkZ71MDq42+NekO
+w2iLBtWFnh7ZxfHITYqvk/yrMMhJLCUGVGzZ47tH5Sj7kiXtnFpK+k58IaIybfJN
+xB1N/IwestjbRMSOJV7dLOxY3qp4tKCShXWZxefcbFM7kURBz8CXlMdGQhIMC+L1
+4DqAPG2mow/zMDMxXwD0AQKBgQCaw82x3cQyuDVScQLeC2NFpkn2r/e7aoXPZOz/
+VNELMP+mkyN5GA/HEeVE6006yggRSFWak5WxACqvjzttoNBZyUERr6oNb6ET0Sm+
+/m3oKllvXqKvokUR5kALQr3Ojibk7namZ2axkQmZYdmmVhMp9EJw+7V8lRXBsuD0
+W/gXjQKBgA/PIPQYsV7YNcB8k+0SZpb3EYGvtgFtw/g8vP2vw+km9QamByhuozup
+d4tFXip+umrXsOR2yeqk3gTs5NbGHtikI46gcbSJKC1j7sEhaVp4dBfmFifYbZB6
+7HsBU+EH2NSH9xW5Y6sIIp0Qc4dP7mfk0tPHP1hMIIB/ai4Exy0I
+-----END RSA PRIVATE KEY-----
diff --git a/testkeys/index.txt b/testkeys/index.txt
new file mode 100644
index 0000000..8079b33
--- /dev/null
+++ b/testkeys/index.txt
@@ -0,0 +1,2 @@
+V 440327194112Z 1000 unknown /C=US/ST=CA/O=Apigee/OU=Engineering/CN=clearserver
+V 440327194424Z 1001 unknown /C=US/ST=CA/O=Apigee/OU=Engineering/CN=securesvr
diff --git a/testkeys/index.txt.attr b/testkeys/index.txt.attr
new file mode 100644
index 0000000..8f7e63a
--- /dev/null
+++ b/testkeys/index.txt.attr
@@ -0,0 +1 @@
+unique_subject = yes
diff --git a/testkeys/serial b/testkeys/serial
new file mode 100644
index 0000000..7d802a3
--- /dev/null
+++ b/testkeys/serial
@@ -0,0 +1 @@
+1002
diff --git a/testkeys/servercert.pem b/testkeys/servercert.pem
new file mode 100644
index 0000000..b7db9b0
--- /dev/null
+++ b/testkeys/servercert.pem
@@ -0,0 +1,113 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number: 4097 (0x1001)
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: C=US, ST=CA, L=San Jose, O=Apigee, OU=Engineering, CN=scaffoldca
+ Validity
+ Not Before: Nov 10 19:44:24 2016 GMT
+ Not After : Mar 27 19:44:24 2044 GMT
+ Subject: C=US, ST=CA, O=Apigee, OU=Engineering, CN=securesvr
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public Key: (2048 bit)
+ Modulus (2048 bit):
+ 00:e5:bf:57:39:9b:9f:d6:b3:94:c0:97:ba:1b:45:
+ af:06:cb:74:9d:15:c0:f6:99:43:10:3d:5f:29:51:
+ f6:1b:3d:7a:40:c7:fb:f2:5f:66:9b:e3:fb:a4:a0:
+ 3a:c3:93:43:4a:cc:d6:3e:a5:4d:c7:00:e0:82:db:
+ 44:79:6e:ad:dc:1f:7a:20:89:cd:e6:4a:d0:f6:9b:
+ 70:a6:b2:02:b5:cf:a1:d0:b6:32:20:71:2d:8d:ea:
+ 0a:17:aa:87:73:25:6a:23:50:85:c7:0b:d5:ae:c3:
+ 1f:4b:2b:3b:d1:51:7c:09:1a:95:4a:f8:a3:89:8b:
+ 23:c5:3c:d4:87:84:5a:48:77:c7:39:97:50:84:eb:
+ f1:eb:cd:9c:45:84:6f:91:31:04:a0:2e:60:23:3a:
+ de:b6:5c:01:47:6c:55:a7:45:f9:6b:6c:8d:0a:a3:
+ b9:76:80:e1:2a:29:15:b4:f7:73:f9:7e:e9:c9:0e:
+ 6b:10:4b:3d:aa:39:09:07:1c:fe:d3:5a:69:f2:e4:
+ 2f:a5:7f:54:f9:d1:68:06:08:3d:f7:81:43:a0:08:
+ c9:90:f6:41:bb:df:1d:ae:e1:c8:8e:51:ae:29:a2:
+ c4:4d:b7:40:20:c9:ba:c3:2d:f9:66:8a:ac:4a:cd:
+ 65:81:49:8b:c1:9e:8b:70:ef:d3:0b:e7:3f:b4:1c:
+ 32:e9
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Basic Constraints:
+ CA:FALSE
+ Netscape Cert Type:
+ SSL Server
+ Netscape Comment:
+ OpenSSL Generated Server Certificate
+ X509v3 Subject Key Identifier:
+ 88:6D:F3:08:34:40:20:72:17:A5:B3:E9:0A:AF:23:9B:88:AE:12:9D
+ X509v3 Authority Key Identifier:
+ keyid:40:E7:D1:A4:A4:FC:B4:26:B0:24:DC:4D:A0:7F:4C:CE:B1:53:AC:2B
+ DirName:/C=US/ST=CA/L=San Jose/O=Apigee/OU=Engineering/CN=scaffoldca
+ serial:F0:4F:F9:FC:E1:F2:CF:40
+
+ X509v3 Key Usage: critical
+ Digital Signature, Key Encipherment
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication
+ Signature Algorithm: sha256WithRSAEncryption
+ 39:3d:26:17:10:c4:c8:81:a2:69:aa:66:c2:9f:b3:ca:78:22:
+ 8b:88:01:23:84:40:4a:ec:07:01:9e:39:04:af:f2:40:c6:35:
+ a4:1b:80:3b:13:5c:30:e8:c5:06:74:87:83:50:fd:13:31:87:
+ f3:05:c7:99:04:59:54:1a:08:fd:3c:43:d6:8b:d1:b1:10:99:
+ 83:3c:7b:c0:a8:d0:15:96:c3:ea:a4:f3:40:88:26:e2:ee:1f:
+ e7:ad:d8:00:d8:ac:e2:33:ed:18:ea:f9:fb:4a:2f:d8:a9:50:
+ fc:88:51:8a:17:14:1f:8c:65:7e:1d:68:0d:63:73:80:f9:a6:
+ d7:b4:0d:20:09:ee:8d:1a:ed:a9:7f:8d:78:e8:f2:0c:47:0c:
+ 9f:4f:0e:0b:a7:66:f1:1c:06:80:bc:2e:c5:b7:b9:2c:37:33:
+ d4:8d:2b:1f:9d:2d:13:07:d7:38:8c:c3:a5:aa:18:2f:e7:5b:
+ 86:48:47:d3:26:63:b3:7e:ee:f3:56:e1:57:5e:08:41:a7:76:
+ 00:c5:54:d6:23:a6:dc:4c:01:31:ad:e8:70:34:cd:89:fc:b7:
+ 7e:73:b0:00:da:8a:d3:f9:8f:0e:12:8c:4a:95:42:4e:a6:52:
+ 95:5e:08:af:69:54:e2:17:3b:d3:c3:3a:60:85:56:6b:e9:a4:
+ 64:f5:0a:d3:9a:ac:30:38:c9:80:cd:be:b1:22:83:7f:45:d7:
+ b4:7a:78:de:bc:45:5f:19:ef:50:2f:bf:72:51:be:ca:72:48:
+ d8:53:3c:f3:79:71:79:72:96:c9:29:96:60:0a:eb:26:1a:08:
+ b5:28:54:55:af:c9:59:64:89:ab:1f:81:4c:8f:3d:24:b4:9c:
+ 3e:28:a8:0a:93:ea:33:dd:24:a4:12:0a:a5:21:55:51:4a:8e:
+ 6d:b9:a3:f6:a2:d0:fe:ba:d5:60:70:0b:4f:79:76:3f:09:47:
+ 9a:de:66:e7:df:76:49:0e:db:74:e1:64:9e:ee:8a:4c:57:80:
+ fd:13:08:e7:5d:90:c0:a4:dd:99:db:aa:bd:ee:ca:24:1d:ad:
+ 51:10:d3:cf:5c:da:e1:af:f1:c4:d1:87:40:8b:d9:b3:c3:72:
+ 9e:24:a4:1e:5a:ec:e4:31:b1:4e:55:ba:c6:55:af:22:1a:62:
+ f2:d1:16:4b:ee:ef:c0:d4:62:85:e3:28:70:e0:1b:1e:8f:e5:
+ bd:36:5d:2b:c7:5a:67:88:c8:9b:5f:55:93:af:62:c2:d9:99:
+ 6d:bc:04:66:00:aa:2d:85:dc:3a:85:3c:8f:c2:3c:b5:7d:16:
+ 36:d6:a3:cc:96:03:67:a7:c3:71:75:2b:71:b4:4f:06:37:15:
+ 2d:d3:44:7a:78:71:53:70
+-----BEGIN CERTIFICATE-----
+MIIFdTCCA12gAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwaTELMAkGA1UEBhMCVVMx
+CzAJBgNVBAgMAkNBMREwDwYDVQQHDAhTYW4gSm9zZTEPMA0GA1UECgwGQXBpZ2Vl
+MRQwEgYDVQQLDAtFbmdpbmVlcmluZzETMBEGA1UEAwwKc2NhZmZvbGRjYTAeFw0x
+NjExMTAxOTQ0MjRaFw00NDAzMjcxOTQ0MjRaMFUxCzAJBgNVBAYTAlVTMQswCQYD
+VQQIDAJDQTEPMA0GA1UECgwGQXBpZ2VlMRQwEgYDVQQLDAtFbmdpbmVlcmluZzES
+MBAGA1UEAwwJc2VjdXJlc3ZyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEA5b9XOZuf1rOUwJe6G0WvBst0nRXA9plDED1fKVH2Gz16QMf78l9mm+P7pKA6
+w5NDSszWPqVNxwDggttEeW6t3B96IInN5krQ9ptwprICtc+h0LYyIHEtjeoKF6qH
+cyVqI1CFxwvVrsMfSys70VF8CRqVSvijiYsjxTzUh4RaSHfHOZdQhOvx682cRYRv
+kTEEoC5gIzretlwBR2xVp0X5a2yNCqO5doDhKikVtPdz+X7pyQ5rEEs9qjkJBxz+
+01pp8uQvpX9U+dFoBgg994FDoAjJkPZBu98druHIjlGuKaLETbdAIMm6wy35Zoqs
+Ss1lgUmLwZ6LcO/TC+c/tBwy6QIDAQABo4IBOTCCATUwCQYDVR0TBAIwADARBglg
+hkgBhvhCAQEEBAMCBkAwMwYJYIZIAYb4QgENBCYWJE9wZW5TU0wgR2VuZXJhdGVk
+IFNlcnZlciBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUiG3zCDRAIHIXpbPpCq8jm4iu
+Ep0wgZsGA1UdIwSBkzCBkIAUQOfRpKT8tCawJNxNoH9MzrFTrCuhbaRrMGkxCzAJ
+BgNVBAYTAlVTMQswCQYDVQQIDAJDQTERMA8GA1UEBwwIU2FuIEpvc2UxDzANBgNV
+BAoMBkFwaWdlZTEUMBIGA1UECwwLRW5naW5lZXJpbmcxEzARBgNVBAMMCnNjYWZm
+b2xkY2GCCQDwT/n84fLPQDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYB
+BQUHAwEwDQYJKoZIhvcNAQELBQADggIBADk9JhcQxMiBommqZsKfs8p4IouIASOE
+QErsBwGeOQSv8kDGNaQbgDsTXDDoxQZ0h4NQ/RMxh/MFx5kEWVQaCP08Q9aL0bEQ
+mYM8e8Co0BWWw+qk80CIJuLuH+et2ADYrOIz7Rjq+ftKL9ipUPyIUYoXFB+MZX4d
+aA1jc4D5pte0DSAJ7o0a7al/jXjo8gxHDJ9PDgunZvEcBoC8LsW3uSw3M9SNKx+d
+LRMH1ziMw6WqGC/nW4ZIR9MmY7N+7vNW4VdeCEGndgDFVNYjptxMATGt6HA0zYn8
+t35zsADaitP5jw4SjEqVQk6mUpVeCK9pVOIXO9PDOmCFVmvppGT1CtOarDA4yYDN
+vrEig39F17R6eN68RV8Z71Avv3JRvspySNhTPPN5cXlylskplmAK6yYaCLUoVFWv
+yVlkiasfgUyPPSS0nD4oqAqT6jPdJKQSCqUhVVFKjm25o/ai0P661WBwC095dj8J
+R5reZuffdkkO23ThZJ7uikxXgP0TCOddkMCk3Znbqr3uyiQdrVEQ089c2uGv8cTR
+h0CL2bPDcp4kpB5a7OQxsU5VusZVryIaYvLRFkvu78DUYoXjKHDgGx6P5b02XSvH
+WmeIyJtfVZOvYsLZmW28BGYAqi2F3DqFPI/CPLV9FjbWo8yWA2enw3F1K3G0TwY3
+FS3TRHp4cVNw
+-----END CERTIFICATE-----
diff --git a/testkeys/servercsr.pem b/testkeys/servercsr.pem
new file mode 100644
index 0000000..3611588
--- /dev/null
+++ b/testkeys/servercsr.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICrTCCAZUCAQAwaDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMREwDwYDVQQH
+DAhTYW4gSm9zZTEPMA0GA1UECgwGQXBpZ2VlMRQwEgYDVQQLDAtFbmdpbmVlcmlu
+ZzESMBAGA1UEAwwJc2VjdXJlc3ZyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA5b9XOZuf1rOUwJe6G0WvBst0nRXA9plDED1fKVH2Gz16QMf78l9mm+P7
+pKA6w5NDSszWPqVNxwDggttEeW6t3B96IInN5krQ9ptwprICtc+h0LYyIHEtjeoK
+F6qHcyVqI1CFxwvVrsMfSys70VF8CRqVSvijiYsjxTzUh4RaSHfHOZdQhOvx682c
+RYRvkTEEoC5gIzretlwBR2xVp0X5a2yNCqO5doDhKikVtPdz+X7pyQ5rEEs9qjkJ
+Bxz+01pp8uQvpX9U+dFoBgg994FDoAjJkPZBu98druHIjlGuKaLETbdAIMm6wy35
+ZoqsSs1lgUmLwZ6LcO/TC+c/tBwy6QIDAQABoAAwDQYJKoZIhvcNAQELBQADggEB
+AJZs+z4Z8G4ofLqPWlgYSs6yKIQJpKwlpBsMVEk/lP8jjjYftJeJg/7ZOEACcAHE
+p+gyfJjuI8Zz5mcqg1YY2d5SUQSXXM43m1YST33oi0rbwONZuP4yq94DwGGmglwq
+XsyI/h8NAO2QWdLS71VazfV2hSNrrVpvhJpB9xa5EFHaTjRMTx2JfsSupXDRqGsu
+w78YO5Hw+PyR1wyoC+l5rzQeLQem3Nt7Hr5uF/GoLeGbdUIM7UdT20huvTHUWCDI
+9J/LPUdmEhtNfdgfpopoiwQsTPyDrKDWhr0DZ2edSc2yzScSV+b7ggIibEAnxRi3
+0XUtHem42cu9DLUoGx6CDTE=
+-----END CERTIFICATE REQUEST-----
diff --git a/testkeys/serverkey.pem b/testkeys/serverkey.pem
new file mode 100644
index 0000000..7b63f75
--- /dev/null
+++ b/testkeys/serverkey.pem
@@ -0,0 +1,30 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-256-CBC,5D08BB5CC1E999C467E431B808DF18DC
+
+sug8HB3HsvaR8j5YFBq4XIYt68jqhTOL5hIv95dI/D/8MLujmvVDXHNB0j1saWJc
+uNX6w9S0/s71FIO1U1jReZv5zeZSsmnmYjnoc4q6fZBGX6rXH22drH781FIkEYjN
+4JbhrhVqcbudRTK+FkSlkDBZO9bRoPSIYuTnf27J+0z885Gq00cNjRnY/nvgo4Vo
+zGTSmC83mk4ckNNPU1fHps16uo39sxRYTllXMs3UEvDvK5VtuWO4CPyJFCaA3omC
+QBszTv9p5Xf2rSrsm/Ay3iVwgjkYxh6EWzcNtQ/vaiQBB+R+ZuZ3+Pg+MO4k6+yO
+OhmXBfbQGcqXAluFhGkew2MbYY7R1ad80YoxXNYKCi+NRqy3HD7nPqY3jR5RdqQe
+kjaBPThqIQHg2mM8ukfG0YNzirBpZ9ApP+LTwNy2RBfJ2TEyG7OB7gR73Cum5uwp
+xdHBe5Vtir0OJuCvZ/MvqIiQi4RmPoTEsAY+9Da1ahKYuEooJxtOg1FLF7JiNI1p
+Kw4K9fS6Td9ZnnQ/0b+mfFQFiw2v8LuWI1NtKTB9smm146ZSPf/vZ8+sV/6zq7TD
+cIpWyrWZl/GDL5Ia+PaZv8ooJk1dVnAgkBtmzQEl0Wu54onavwqn43z7qBgpxngc
+Pa5AddTnUugH/8MR82R9xbzfplm6yHSB2hQs9mQ8WEXeAolNoVQweaRhgabVXAkB
+Y25sCCKlKLwneOvVPIFUWuKGtUOj+WMqXJhcQFxf6ezaOy2p/H/DqY2m1eqOUvf3
+48MlZ6cu4wzOOuIbYxTBipsWPH3rCHt1NbiNjLcT3eoDxT9aEqHGRDfzrVllD1x6
+oOm/DR/QMQuREHiFrhaWg95boHfpd0+fcHYykfPsWqET/EMx7MSG1RFNKvBsWwWr
+AOXjLMfUFnmyGv/f/3IaZV236R625kKhPqRjSIUFGZ/YoFTWh+k0bGwHRItXVsha
+DP5L/3ilOs+mgRVA2B8l9no50rLxc/T4C+8JYsokDABqt92n8Gr4UTJiIMf+YKzF
+Qt3GIMCBrhH/51dY2tNi/rAOxd8evXVE2EDDHK3MqAwwxvKp0WhXc8j/vK6wqDJH
+BEwumbx/uSZyXB8uW7b7jIzswzVW4FGmTnYN9CjMY9xNW+PoVWHlqRTZVSRnhhpq
+YJKozl7cr+Uv4S4scHwJbglACmGSc2yYhI2JGY0vZ3KnamNbdumXwbyMvuqnT+WM
+JXHHjI2txnnWSxXoQREXVzJJp+5aBfCpnOIi/tX4ocjEPtnHXXLGNRtkp00ig/DY
+/AVKkLDP2RfElbfxz8AicgCH4PlEwmgPvtqmrsUCeMkK2dIoamqysnCvqZzNPuSS
+CGo2mwbwefiacoeCf+UhKmyQtQ0uLkrG6na3q0/LSHT/2cAU+P4Q60d2Iq0B+rF8
+8jhX1OrDvZZFEvrvgD5Fbfhmc6YjphDFPc7hey/hQ74s3QR+fSbiGknpAWxBYelu
+OMgo4OW+T598Im5Oa5z3KH1rcv7dPtf9SIl1dk09t00bg/RNp/KTpxXGh8WevFXd
+NmmN/VLzaGdxVT+HUojLJMUt74RomLDFbresoi72PHPt+CUjZpJSkO63uwb80Jyw
+-----END RSA PRIVATE KEY-----