Extend certauthority to support loading an existing CA.

I think we may still split this apart into multiple packages, but for now it works pretty well in both use cases.

Signed-off-by: Matt Moyer <moyerm@vmware.com>
This commit is contained in:
Matt Moyer 2020-07-27 07:50:59 -05:00
parent 899f736b8c
commit 757d987204
8 changed files with 394 additions and 125 deletions

View File

@ -22,8 +22,7 @@ import (
"time"
)
// CA holds the state for a simple x509 certificate authority suitable for use in an aggregated API service.
type CA struct {
type env struct {
// secure random number generators for various steps (usually crypto/rand.Reader, but broken out here for tests).
serialRNG io.Reader
keygenRNG io.Reader
@ -32,45 +31,70 @@ type CA struct {
// clock tells the current time (usually time.Now(), but broken out here for tests).
clock func() time.Time
// parse function to parse an ASN.1 byte slice into an x509 struct (normally x509.ParseCertificate)
parseCert func([]byte) (*x509.Certificate, error)
}
// CA holds the state for a simple x509 certificate authority suitable for use in an aggregated API service.
type CA struct {
// caCert is the DER-encoded certificate for the current CA.
caCertBytes []byte
// signer is the private key for the current CA.
signer crypto.Signer
// caCert is the DER-encoded certificate for the current CA.
caCertBytes []byte
// env is our reference to the outside world (clocks and random number generation).
env env
}
// Option to pass when calling New.
type Option func(*CA) error
func New(subject pkix.Name, opts ...Option) (*CA, error) {
// Initialize the result by starting with some defaults and applying any provided options.
ca := CA{
// secureEnv is the "real" environment using secure RNGs and the real system clock.
func secureEnv() env {
return env{
serialRNG: rand.Reader,
keygenRNG: rand.Reader,
signingRNG: rand.Reader,
clock: time.Now,
}
for _, opt := range opts {
if err := opt(&ca); err != nil {
return nil, err
parseCert: x509.ParseCertificate,
}
}
// Load a certificate authority from an existing certificate and private key (in PEM format).
func Load(certPath string, keyPath string) (*CA, error) {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, fmt.Errorf("could not load CA: %w", err)
}
if certCount := len(cert.Certificate); certCount != 1 {
return nil, fmt.Errorf("expected CA to be a single certificate, found %d certificates", certCount)
}
return &CA{
caCertBytes: cert.Certificate[0],
signer: cert.PrivateKey.(crypto.Signer),
env: secureEnv(),
}, nil
}
// New generates a fresh certificate authority with the given subject.
func New(subject pkix.Name) (*CA, error) { return newInternal(subject, secureEnv()) }
// newInternal is the internal guts of New, broken out for easier testing.
func newInternal(subject pkix.Name, env env) (*CA, error) {
ca := CA{env: env}
// Generate a random serial for the CA
serialNumber, err := randomSerial(ca.serialRNG)
serialNumber, err := randomSerial(env.serialRNG)
if err != nil {
return nil, fmt.Errorf("could not generate CA serial: %w", err)
}
// Generate a new P256 keypair.
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), ca.keygenRNG)
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), env.keygenRNG)
if err != nil {
return nil, fmt.Errorf("could not generate CA private key: %w", err)
}
ca.signer = privateKey
// Make a CA certificate valid for 100 years and backdated by one minute.
now := ca.clock()
now := env.clock()
notBefore := now.Add(-1 * time.Minute)
notAfter := now.Add(24 * time.Hour * 365 * 100)
@ -87,7 +111,7 @@ func New(subject pkix.Name, opts ...Option) (*CA, error) {
}
// Self-sign the CA to get the DER certificate.
caCertBytes, err := x509.CreateCertificate(ca.signingRNG, &caTemplate, &caTemplate, &privateKey.PublicKey, privateKey)
caCertBytes, err := x509.CreateCertificate(env.signingRNG, &caTemplate, &caTemplate, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, fmt.Errorf("could not issue CA certificate: %w", err)
}
@ -95,37 +119,27 @@ func New(subject pkix.Name, opts ...Option) (*CA, error) {
return &ca, nil
}
// WriteBundle writes the current CA signing bundle in concatenated PEM format.
func (c *CA) WriteBundle(out io.Writer) error {
if err := pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: c.caCertBytes}); err != nil {
return fmt.Errorf("could not encode CA certificate to PEM: %w", err)
}
return nil
}
// Bundle returns the current CA signing bundle in concatenated PEM format.
func (c *CA) Bundle() ([]byte, error) {
var out bytes.Buffer
err := c.WriteBundle(&out)
return out.Bytes(), err
func (c *CA) Bundle() []byte {
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.caCertBytes})
}
// Issue a new server certificate for the given identity and duration.
func (c *CA) Issue(subject pkix.Name, dnsNames []string, ttl time.Duration) (*tls.Certificate, error) {
// Choose a random 128 bit serial number.
serialNumber, err := randomSerial(c.serialRNG)
serialNumber, err := randomSerial(c.env.serialRNG)
if err != nil {
return nil, fmt.Errorf("could not generate serial number for certificate: %w", err)
}
// Generate a new P256 keypair.
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), c.keygenRNG)
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), c.env.keygenRNG)
if err != nil {
return nil, fmt.Errorf("could not generate private key: %w", err)
}
// Make a CA caCert valid for the requested TTL and backdated by one minute.
now := c.clock()
now := c.env.clock()
notBefore := now.Add(-1 * time.Minute)
notAfter := now.Add(ttl)
@ -153,7 +167,7 @@ func (c *CA) Issue(subject pkix.Name, dnsNames []string, ttl time.Duration) (*tl
}
// Parse the DER encoded certificate back out into an *x509.Certificate.
newCert, err := x509.ParseCertificate(certBytes)
newCert, err := c.env.parseCert(certBytes)
if err != nil {
return nil, fmt.Errorf("could not parse certificate: %w", err)
}
@ -166,6 +180,35 @@ func (c *CA) Issue(subject pkix.Name, dnsNames []string, ttl time.Duration) (*tl
}, nil
}
// IssuePEM issues a new server certificate for the given identity and duration, returning it as a pair of
// PEM-formatted byte slices for the certificate and private key.
func (c *CA) IssuePEM(subject pkix.Name, dnsNames []string, ttl time.Duration) ([]byte, []byte, error) {
return toPEM(c.Issue(subject, dnsNames, ttl))
}
func toPEM(cert *tls.Certificate, err error) ([]byte, []byte, error) {
// If the wrapped Issue() returned an error, pass it back.
if err != nil {
return nil, nil, err
}
// Encode the certificate(s) to PEM.
certPEMBlocks := make([][]byte, 0, len(cert.Certificate))
for _, c := range cert.Certificate {
certPEMBlocks = append(certPEMBlocks, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c}))
}
certPEM := bytes.Join(certPEMBlocks, nil)
// Encode the private key to PEM, which means we first need to convert to PKCS8 (DER).
privateKeyPKCS8, err := x509.MarshalPKCS8PrivateKey(cert.PrivateKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal private key into PKCS8: %w", err)
}
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyPKCS8})
return certPEM, keyPEM, nil
}
// randomSerial generates a random 128 bit serial number.
func randomSerial(rng io.Reader) (*big.Int, error) {
return rand.Int(rng, new(big.Int).Lsh(big.NewInt(1), 128))

View File

@ -6,8 +6,8 @@ SPDX-License-Identifier: Apache-2.0
package certauthority
import (
"bytes"
"crypto"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
@ -19,65 +19,149 @@ import (
"github.com/stretchr/testify/require"
)
func TestNew(t *testing.T) {
now := time.Date(2020, 7, 10, 12, 41, 12, 1234, time.UTC)
func TestLoad(t *testing.T) {
tests := []struct {
name string
opts []Option
certPath string
keyPath string
wantErr string
}{
{
name: "error option",
opts: []Option{func(ca *CA) error {
return fmt.Errorf("some error")
}},
wantErr: "some error",
name: "missing cert",
certPath: "./testdata/cert-does-not-exist",
keyPath: "./testdata/test.key",
wantErr: "could not load CA: open ./testdata/cert-does-not-exist: no such file or directory",
},
{
name: "failed to generate CA serial",
opts: []Option{func(ca *CA) error {
ca.serialRNG = strings.NewReader("")
ca.keygenRNG = strings.NewReader("")
ca.signingRNG = strings.NewReader("")
return nil
}},
wantErr: "could not generate CA serial: EOF",
name: "empty cert",
certPath: "./testdata/empty",
keyPath: "./testdata/test.key",
wantErr: "could not load CA: tls: failed to find any PEM data in certificate input",
},
{
name: "failed to generate CA key",
opts: []Option{func(ca *CA) error {
ca.serialRNG = strings.NewReader(strings.Repeat("x", 64))
ca.keygenRNG = strings.NewReader("")
return nil
}},
wantErr: "could not generate CA private key: EOF",
name: "invalid cert",
certPath: "./testdata/invalid",
keyPath: "./testdata/test.key",
wantErr: "could not load CA: tls: failed to find any PEM data in certificate input",
},
{
name: "failed to self-sign",
opts: []Option{func(ca *CA) error {
ca.serialRNG = strings.NewReader(strings.Repeat("x", 64))
ca.keygenRNG = strings.NewReader(strings.Repeat("y", 64))
ca.signingRNG = strings.NewReader("")
return nil
}},
wantErr: "could not issue CA certificate: EOF",
name: "missing key",
certPath: "./testdata/test.crt",
keyPath: "./testdata/key-does-not-exist",
wantErr: "could not load CA: open ./testdata/key-does-not-exist: no such file or directory",
},
{
name: "empty key",
certPath: "./testdata/test.crt",
keyPath: "./testdata/empty",
wantErr: "could not load CA: tls: failed to find any PEM data in key input",
},
{
name: "invalid key",
certPath: "./testdata/test.crt",
keyPath: "./testdata/invalid",
wantErr: "could not load CA: tls: failed to find any PEM data in key input",
},
{
name: "mismatched cert and key",
certPath: "./testdata/test.crt",
keyPath: "./testdata/test2.key",
wantErr: "could not load CA: tls: private key does not match public key",
},
{
name: "multiple certs",
certPath: "./testdata/multiple.crt",
keyPath: "./testdata/test.key",
wantErr: "expected CA to be a single certificate, found 2 certificates",
},
{
name: "success",
opts: []Option{func(ca *CA) error {
ca.serialRNG = strings.NewReader(strings.Repeat("x", 64))
ca.keygenRNG = strings.NewReader(strings.Repeat("y", 64))
ca.signingRNG = strings.NewReader(strings.Repeat("z", 64))
ca.clock = func() time.Time { return now }
return nil
}},
certPath: "./testdata/test.crt",
keyPath: "./testdata/test.key",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
got, err := New(pkix.Name{CommonName: "Test CA"}, tt.opts...)
ca, err := Load(tt.certPath, tt.keyPath)
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
return
}
require.NoError(t, err)
require.NotEmpty(t, ca.caCertBytes)
require.NotNil(t, ca.signer)
})
}
}
func TestNew(t *testing.T) {
got, err := New(pkix.Name{CommonName: "Test CA"})
require.NoError(t, err)
require.NotNil(t, got)
// Make sure the CA certificate looks roughly like what we expect.
caCert, err := x509.ParseCertificate(got.caCertBytes)
require.NoError(t, err)
require.Equal(t, "Test CA", caCert.Subject.CommonName)
}
func TestNewInternal(t *testing.T) {
now := time.Date(2020, 7, 10, 12, 41, 12, 1234, time.UTC)
tests := []struct {
name string
env env
wantErr string
wantCommonName string
wantNotBefore time.Time
wantNotAfter time.Time
}{
{
name: "failed to generate CA serial",
env: env{
serialRNG: strings.NewReader(""),
keygenRNG: strings.NewReader(""),
signingRNG: strings.NewReader(""),
},
wantErr: "could not generate CA serial: EOF",
},
{
name: "failed to generate CA key",
env: env{
serialRNG: strings.NewReader(strings.Repeat("x", 64)),
keygenRNG: strings.NewReader(""),
signingRNG: strings.NewReader(""),
},
wantErr: "could not generate CA private key: EOF",
},
{
name: "failed to self-sign",
env: env{
serialRNG: strings.NewReader(strings.Repeat("x", 64)),
keygenRNG: strings.NewReader(strings.Repeat("y", 64)),
signingRNG: strings.NewReader(""),
clock: func() time.Time { return now },
},
wantErr: "could not issue CA certificate: EOF",
},
{
name: "success",
env: env{
serialRNG: strings.NewReader(strings.Repeat("x", 64)),
keygenRNG: strings.NewReader(strings.Repeat("y", 64)),
signingRNG: strings.NewReader(strings.Repeat("z", 64)),
clock: func() time.Time { return now },
},
wantCommonName: "Test CA",
wantNotAfter: now.Add(100 * 365 * 24 * time.Hour),
wantNotBefore: now.Add(-1 * time.Minute),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
got, err := newInternal(pkix.Name{CommonName: "Test CA"}, tt.env)
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
require.Nil(t, got)
@ -89,46 +173,17 @@ func TestNew(t *testing.T) {
// Make sure the CA certificate looks roughly like what we expect.
caCert, err := x509.ParseCertificate(got.caCertBytes)
require.NoError(t, err)
require.Equal(t, "Test CA", caCert.Subject.CommonName)
require.Equal(t, now.Add(100*365*24*time.Hour).Unix(), caCert.NotAfter.Unix())
require.Equal(t, now.Add(-1*time.Minute).Unix(), caCert.NotBefore.Unix())
require.Equal(t, tt.wantCommonName, caCert.Subject.CommonName)
require.Equal(t, tt.wantNotAfter.Unix(), caCert.NotAfter.Unix())
require.Equal(t, tt.wantNotBefore.Unix(), caCert.NotBefore.Unix())
})
}
}
type errWriter struct {
err error
}
func (e *errWriter) Write(p []byte) (n int, err error) { return 0, e.err }
func TestWriteBundle(t *testing.T) {
t.Run("error", func(t *testing.T) {
ca := CA{}
out := errWriter{fmt.Errorf("some error")}
require.EqualError(t, ca.WriteBundle(&out), "could not encode CA certificate to PEM: some error")
})
t.Run("empty", func(t *testing.T) {
ca := CA{}
var out bytes.Buffer
require.NoError(t, ca.WriteBundle(&out))
require.Equal(t, "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----\n", out.String())
})
t.Run("success", func(t *testing.T) {
ca := CA{caCertBytes: []byte{1, 2, 3, 4, 5, 6, 7, 8}}
var out bytes.Buffer
require.NoError(t, ca.WriteBundle(&out))
require.Equal(t, "-----BEGIN CERTIFICATE-----\nAQIDBAUGBwg=\n-----END CERTIFICATE-----\n", out.String())
})
}
func TestBundle(t *testing.T) {
t.Run("success", func(t *testing.T) {
ca := CA{caCertBytes: []byte{1, 2, 3, 4, 5, 6, 7, 8}}
got, err := ca.Bundle()
require.NoError(t, err)
got := ca.Bundle()
require.Equal(t, "-----BEGIN CERTIFICATE-----\nAQIDBAUGBwg=\n-----END CERTIFICATE-----\n", string(got))
})
}
@ -147,7 +202,7 @@ func (e *errSigner) Sign(_ io.Reader, _ []byte, _ crypto.SignerOpts) ([]byte, er
func TestIssue(t *testing.T) {
now := time.Date(2020, 7, 10, 12, 41, 12, 1234, time.UTC)
realCA, err := New(pkix.Name{CommonName: "Test CA"})
realCA, err := Load("./testdata/test.crt", "./testdata/test.key")
require.NoError(t, err)
tests := []struct {
@ -158,33 +213,41 @@ func TestIssue(t *testing.T) {
{
name: "failed to generate serial",
ca: CA{
env: env{
serialRNG: strings.NewReader(""),
},
},
wantErr: "could not generate serial number for certificate: EOF",
},
{
name: "failed to generate keypair",
ca: CA{
env: env{
serialRNG: strings.NewReader(strings.Repeat("x", 64)),
keygenRNG: strings.NewReader(""),
},
},
wantErr: "could not generate private key: EOF",
},
{
name: "invalid CA certificate",
ca: CA{
env: env{
serialRNG: strings.NewReader(strings.Repeat("x", 64)),
keygenRNG: strings.NewReader(strings.Repeat("x", 64)),
clock: func() time.Time { return now },
},
},
wantErr: "could not parse CA certificate: asn1: syntax error: sequence truncated",
},
{
name: "signing error",
ca: CA{
env: env{
serialRNG: strings.NewReader(strings.Repeat("x", 64)),
keygenRNG: strings.NewReader(strings.Repeat("x", 64)),
clock: func() time.Time { return now },
},
caCertBytes: realCA.caCertBytes,
signer: &errSigner{
pubkey: realCA.signer.Public(),
@ -196,9 +259,28 @@ func TestIssue(t *testing.T) {
{
name: "success",
ca: CA{
env: env{
serialRNG: strings.NewReader(strings.Repeat("x", 64)),
keygenRNG: strings.NewReader(strings.Repeat("x", 64)),
clock: func() time.Time { return now },
parseCert: func(_ []byte) (*x509.Certificate, error) {
return nil, fmt.Errorf("some parse certificate error")
},
},
caCertBytes: realCA.caCertBytes,
signer: realCA.signer,
},
wantErr: "could not parse certificate: some parse certificate error",
},
{
name: "success",
ca: CA{
env: env{
serialRNG: strings.NewReader(strings.Repeat("x", 64)),
keygenRNG: strings.NewReader(strings.Repeat("x", 64)),
clock: func() time.Time { return now },
parseCert: x509.ParseCertificate,
},
caCertBytes: realCA.caCertBytes,
signer: realCA.signer,
},
@ -218,3 +300,41 @@ func TestIssue(t *testing.T) {
})
}
}
func TestIssuePEM(t *testing.T) {
realCA, err := Load("./testdata/test.crt", "./testdata/test.key")
require.NoError(t, err)
certPEM, keyPEM, err := realCA.IssuePEM(pkix.Name{CommonName: "Test Server"}, []string{"example.com"}, 10*time.Minute)
require.NoError(t, err)
require.NotEmpty(t, certPEM)
require.NotEmpty(t, keyPEM)
}
func TestToPEM(t *testing.T) {
realCert, err := tls.LoadX509KeyPair("./testdata/test.crt", "./testdata/test.key")
require.NoError(t, err)
t.Run("error from input", func(t *testing.T) {
certPEM, keyPEM, err := toPEM(nil, fmt.Errorf("some error"))
require.EqualError(t, err, "some error")
require.Nil(t, certPEM)
require.Nil(t, keyPEM)
})
t.Run("invalid private key", func(t *testing.T) {
cert := realCert
cert.PrivateKey = nil
certPEM, keyPEM, err := toPEM(&cert, nil)
require.EqualError(t, err, "failed to marshal private key into PKCS8: x509: unknown key type while marshaling PKCS#8: <nil>")
require.Nil(t, certPEM)
require.Nil(t, keyPEM)
})
t.Run("success", func(t *testing.T) {
certPEM, keyPEM, err := toPEM(&realCert, nil)
require.NoError(t, err)
require.NotEmpty(t, certPEM)
require.NotEmpty(t, keyPEM)
})
}

0
internal/certauthority/testdata/empty vendored Normal file
View File

View File

@ -0,0 +1 @@
Some invalid file contents

View File

@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
MIICyDCCAbCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl
cm5ldGVzMB4XDTIwMDcyNTIxMDQxOFoXDTMwMDcyMzIxMDQxOFowFTETMBEGA1UE
AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3K
hYv2gIQ1Dwzh2cWMid+ofAnvLIfV2Xv61vTLGprUI+XUqB4/gtf6X6UNn0Lett2n
d8p4wy7hw73hU/ggdvmWJvqBrSjc3JGfy+kj66fKXX+PTlbL7QbwiRvcSqIXIWlV
lHHxECWrED8jCulw/NVqfook/h5iNUCT9yswSJr/0fImiVnoTlIoEYG2eCNejZ5c
g39uD3ZTqd9ZxWwSLLnI+2kpJnZBPcd1ZQ8AQqzDgZtYRCqacn5gckQUKZWKQlxo
Eft6g1XHJouAWAZw7hEtk0v8rG0/eKF7wamxFi6BFVlbjWBsB4T9rApbdBWTKeCJ
Hv8fv5RMFSzpT3uzTO8CAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB
/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBACh5RhbxqJe+Z/gc17cZhKNmdiwu
I2pLp3QBfwvN+Wbmajzw/7rYhY0d8JYVTJzXSCPWi6UAKxAtXOLF8WIIf9i39n6R
uKOBGW14FzzGyRJiD3qaG/JTvEW+SLhwl68Ndr5LHSnbugAqq31abcQy6Zl9v5A8
JKC97Lj/Sn8rj7opKy4W3oq7NCQsAb0zh4IllRF6UvSnJySfsg7xdXHHpxYDHtOS
XcOu5ySUIZTgFe9RfeUZlGZ5xn0ckMlQ7qW2Wx1q0OVWw5us4NtkGqKrHG4Tn1X7
uwo/Yytn5sDxrDv1/oii6AZOCsTPre4oD3wz4nmVzCVJcgrqH4Q24hT8WNg=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICyzCCAbOgAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl
cm5ldGVzMB4XDTIwMDcyMTIxMDcwMloXDTMwMDcxOTIxMTIwMlowFTETMBEGA1UE
AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALRz
y8TRjCTkqxTGpMoGa6nmZAZU76GqchVJh6x/T/utsfE822IgyLo5oNKKtLsNiXOT
pO1DdaPtwKJBlifmAIfPwPIOikcqYicLYMS6PM1f6pPGUM9zlYW198sQfTybVVQG
KBLEjyYbcFPtM98omXaILM+V8KXokMiSWgho36qH/abcdxKKTPJs44U+mby5gkew
Iee+8yXOiqCmCPc6a+UsU28eYt72XTiAMLKQsGu71Q99IMXYq6/kZbxxFk8zh2r+
ZuaBbCbasclznutLnWB4hpfswLnTY3Ct9kv07mjzmIsLguOOQGzoVJ72xsLwxfoG
KS5jnTImXFVp28NFP6kCAwEAAaMmMCQwDgYDVR0PAQH/BAQDAgKkMBIGA1UdEwEB
/wQIMAYBAf8CAQAwDQYJKoZIhvcNAQELBQADggEBAF4E3IA8Wst9GOeFjjv/IPAp
LT89JHehMrTRMngw+Lvv4WG9rymtHFpvEiVgh8MhcP+MuNc95tIPLaonpTdKJ/dR
PgDlpOFL0dvaskuOVWQYZnD/ehf8zF+vlt9Mby3rJleruGETahdvQVd7DB61zSC6
7g2rE2VqZ62Vj48RdEF9n+tYXWa4wRw3I1i8DUA/fNAS8P4/KXN6VvdVHe8y+nim
ASs8VX0QEWaGRIQZankFXv5p1/tXOFo/Wmo8vrVa9OsCXJm7iN5riwYiQn237iBJ
XmEs5jp04H7V7aDqZOX/N13uzQbGSPChAgWr/LxO1tIa6PmK+vfo60lRsBWeYrE=
-----END CERTIFICATE-----

View File

@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICyDCCAbCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl
cm5ldGVzMB4XDTIwMDcyNTIxMDQxOFoXDTMwMDcyMzIxMDQxOFowFTETMBEGA1UE
AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3K
hYv2gIQ1Dwzh2cWMid+ofAnvLIfV2Xv61vTLGprUI+XUqB4/gtf6X6UNn0Lett2n
d8p4wy7hw73hU/ggdvmWJvqBrSjc3JGfy+kj66fKXX+PTlbL7QbwiRvcSqIXIWlV
lHHxECWrED8jCulw/NVqfook/h5iNUCT9yswSJr/0fImiVnoTlIoEYG2eCNejZ5c
g39uD3ZTqd9ZxWwSLLnI+2kpJnZBPcd1ZQ8AQqzDgZtYRCqacn5gckQUKZWKQlxo
Eft6g1XHJouAWAZw7hEtk0v8rG0/eKF7wamxFi6BFVlbjWBsB4T9rApbdBWTKeCJ
Hv8fv5RMFSzpT3uzTO8CAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB
/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBACh5RhbxqJe+Z/gc17cZhKNmdiwu
I2pLp3QBfwvN+Wbmajzw/7rYhY0d8JYVTJzXSCPWi6UAKxAtXOLF8WIIf9i39n6R
uKOBGW14FzzGyRJiD3qaG/JTvEW+SLhwl68Ndr5LHSnbugAqq31abcQy6Zl9v5A8
JKC97Lj/Sn8rj7opKy4W3oq7NCQsAb0zh4IllRF6UvSnJySfsg7xdXHHpxYDHtOS
XcOu5ySUIZTgFe9RfeUZlGZ5xn0ckMlQ7qW2Wx1q0OVWw5us4NtkGqKrHG4Tn1X7
uwo/Yytn5sDxrDv1/oii6AZOCsTPre4oD3wz4nmVzCVJcgrqH4Q24hT8WNg=
-----END CERTIFICATE-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAvcqFi/aAhDUPDOHZxYyJ36h8Ce8sh9XZe/rW9MsamtQj5dSo
Hj+C1/pfpQ2fQt623ad3ynjDLuHDveFT+CB2+ZYm+oGtKNzckZ/L6SPrp8pdf49O
VsvtBvCJG9xKohchaVWUcfEQJasQPyMK6XD81Wp+iiT+HmI1QJP3KzBImv/R8iaJ
WehOUigRgbZ4I16NnlyDf24PdlOp31nFbBIsucj7aSkmdkE9x3VlDwBCrMOBm1hE
KppyfmByRBQplYpCXGgR+3qDVccmi4BYBnDuES2TS/ysbT94oXvBqbEWLoEVWVuN
YGwHhP2sClt0FZMp4Ike/x+/lEwVLOlPe7NM7wIDAQABAoIBAFC1tUEmHNUcM0BJ
M3D9KQzB+63F1mwVlx1QOOV1EeVR3co5Ox1R6PSr9sycFGQ9jgqI0zp5TJe9Tp6L
GkhklfPh1MWnK9o6wlnzWKXWrrp2Jni+mpPyuOPAmq4Maniv2XeP+0bROwqpyojv
AA7yC7M+TH226ZJGNVs3EV9+cwHml0yuzBfIJn/rv/w2g+WRKM/MC0S7k2d8bRlA
NycKVGAGBhKTltjoVYOeh6aHEpSjK8zfaePjo5dYJvoVIli60YCgcJOU/8jXT+Np
1Fm7tRvAtj3pUp0Sqdaf2RUzh9jfJp2VFCHuSJ6TPqArOyQojtMcTHF0TiW7xrHP
xOCRIAECgYEAwGBPU7vdthMJBg+ORUoGQQaItTeJvQwIqJvbKD2osp4jhS1dGZBw
W30GKEc/gd8JNtOq9BBnMicPF7hktuy+bSPv41XPud67rSSO7Tsw20C10gFRq06B
zIJWFAUqK3IkvVc3VDmtSLSDox4QZ/BdqaMlQ5y5JCsC5kThmkZFlO8CgYEA/I9X
YHi6RioMJE1fqOHJL4DDjlezmcuRrD7fE5InKbtJZ2JhGYOX/C0KXnHTOWTCDxxN
FBvpvD6Xv5o3PhB9Z6k2fqvJ4GS8urkG/KU4xcC+bak+9ava8oaiSqG16zD9NH2P
jJ60NrbLl1J0pU9fiwuFVUKJ4hDZOfN9RqYdyAECgYAVwo8WhJiGgM6zfcz073OX
pVqPTPHqjVLpZ3+5pIfRdGvGI6R1QM5EuvaYVb7MPOM47WZX5wcVOC/P2g6iVlMP
21HGIC2384a9BfaYxOo40q/+SiHnw6CQ9mkwKIllkqqvNA9RGpkMMUb2i28For2l
c4vCgxa6DZdtXns6TRqPxwKBgCfY5cxOv/T6BVhk7MbUeM2J31DB/ZAyUhV/Bess
kAlBh19MYk2IOZ6L7KriApV3lDaWHIMjtEkDByYvyq98Io0MYZCywfMpca10K+oI
l2B7/I+IuGpCZxUEsO5dfTpSTGDPvqpND9niFVUWqVi7oTNq6ep9yQtl5SADjqxq
4SABAoGAIm0hUg1wtcS46cGLy6PIkPM5tocTSghtz4vFsuk/i4QA9GBoBO2gH6ty
+kJHmeaXt2dmgySp0QAWit5UlceEumB0NXnAdJZQxeGSFSyYkDWhwXd8wDceKo/1
LfCU6Dk8IN/SsppVUWXQ2rlORvxlrHeCio8o0kS9Yiu55WMYg4g=
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAqeddVCjzZNKp/hVprYRY4EKos0PkrpkA0cbQTIhKrJIU2JXW
G0/RgffNnfMEXGR/6xhGPGM9CU+jD5aZJtl/+7w0zhlZib1lQLeSIfOLGO4rqbsr
k4KXYosUaUD8qd6El1FXUB6OfpZFjmvE5qzFlpjOEbedDrgbzVeD6BsdeCrgT3G4
Jly/79MWsOjGk8Oa+OS/4OLf6W0UnKCWbyENNDVrsihLByffv1YZZ+s4bOMFb1cA
Oc078TF/1M5ekTcicGdKKxr7CQT9RtR6iuPcVucuBhd9eDCsK4tH9PEWtyY/hISz
Pnrigt6LEW++IvHLJQ7mX+COzFMNramyK/F8aQIDAQABAoIBAFUd02OWIFkiMIdZ
std6tgujWWB1YtsVS5PMRg4ROVe61zap2dlU42B5BElctZKTxoHAZ29ZR/qiKs5k
Y9VSoQs7/jhB+tlGSLNjQ5I+sDCNINKnMe10PuLfShpwtCNlloc3+MXqiPhhz/bJ
hpsJcvM/Gf1GPyhgk40Lisl8zAamouLZ0qQSspmHmPo4ztpA2w81bG05GfTJTqVp
6pceOotVU4NNkiqq0SOF13ZajUaR3ifCnEbjkRnh8L8x0rqGUv7aAZMIc6x3kHRT
ZG2HfLvQW8QWsOWB6WIdZqAPMtrTJw0ozMfLJYfkfosGjLC99pjefMY7ENPnGwvN
FdOgwAECgYEA3WtNyi9bE5Licrv0Ys/A3oI11in2eKL8cmeJM/Hxvxz3EzQH8Kl8
nYtfCP18kHSg7fddg4xbqW6NLPIkOtopDy0evTzPpZpxBc6mu1buqFm4EBeUAsIu
rK1Ol0dKxZ7EoFAQGHN0JpTd62diRHnxCiIvfQD55ClloyrR/Cd8SuECgYEAxHBk
YsDSFs9DFbhvFEWigfPPYjViHg9FsNBYodtuiyWXbkH8QXmjTpEaiLXk5Zc0VOYA
8OBYbxly1l6nAW9/v2LewPoxVQTyTnTnsLlarLo2ax8GT1NNu9gCPGzbEORvlHky
h/NCw0RBcmGnfTijoOVEC8RETbN1SK3x5as0qokCgYAM3myyAJiZhaL1qijlCVAb
XpQEc4HotwhXGd9mjnxPcD6H9jEz8pXUjkIiwqDXwH+N9R+RQrodGdjIsPYcGYvj
Xur3cq5a4KQLA1y7bK0ISdah0M0AcArIbHYx4qnc3IJvEtgso6EvkN1pDiQu+Kti
vGPoLwNXGHTYy+dScXUO4QKBgQCmgFFGNwOb28+T0IEuYJuOpJZKOs9QhUdfyCjo
ADMhdBp3lSx4Xt6h0HH6IJrEU7ZCo7V2deHfQWXJ9+58VAKmuOnwDeDUnF25THO5
olIOB8PqZiCWChjgOAYlK2s/VTCSW2wOOY2ELw1+IvGxPNnMnadghdoTNiIaGX3o
WoZIaQKBgEDojPr84GMR+M5d2YrvLJmLSmDLwWR0j6soANUXsaLzZVZBifOSKIG8
ySO4QOe4O2Nb1nWAfmEP964VwoQ0rHlkt0rJopwJTj4uYjhDRGIN+x2aJZ00bm4u
/1c7cowqxj/TkjTP4JASxBwBPUYpJxiajATTZiddxt4xRxaNVKlK
-----END RSA PRIVATE KEY-----