6f66e5cd48
* Allow reading Nomad CA/Client cert configuration In the Nomad secret engine, writing to /nomad/config/access allows users to specify a CA certificate and client credential pair. However, these values are not in the read of the endpoint, making it hard for operators to see if these values were specified and if they need to be rotated. Add `ca_cert` and `client_cert` parameters to the response, eliding the `client_key` parameter as it is more sensitive (and should most likely be replaced at the same time as `client_cert`). Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add changelog Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Fix tests to expect additional fields Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add test with existing CA/client cert+key Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
706 lines
21 KiB
Go
706 lines
21 KiB
Go
package nomad
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
nomadapi "github.com/hashicorp/nomad/api"
|
|
"github.com/hashicorp/vault/helper/testhelpers"
|
|
"github.com/hashicorp/vault/helper/testhelpers/docker"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
"github.com/mitchellh/mapstructure"
|
|
)
|
|
|
|
type Config struct {
|
|
docker.ServiceURL
|
|
Token string
|
|
}
|
|
|
|
func (c *Config) APIConfig() *nomadapi.Config {
|
|
apiConfig := nomadapi.DefaultConfig()
|
|
apiConfig.Address = c.URL().String()
|
|
apiConfig.SecretID = c.Token
|
|
return apiConfig
|
|
}
|
|
|
|
func (c *Config) Client() (*nomadapi.Client, error) {
|
|
apiConfig := c.APIConfig()
|
|
|
|
return nomadapi.NewClient(apiConfig)
|
|
}
|
|
|
|
func prepareTestContainer(t *testing.T, bootstrap bool) (func(), *Config) {
|
|
if retAddress := os.Getenv("NOMAD_ADDR"); retAddress != "" {
|
|
s, err := docker.NewServiceURLParse(retAddress)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return func() {}, &Config{*s, os.Getenv("NOMAD_TOKEN")}
|
|
}
|
|
|
|
runner, err := docker.NewServiceRunner(docker.RunOptions{
|
|
ImageRepo: "multani/nomad",
|
|
ImageTag: "1.1.6",
|
|
ContainerName: "nomad",
|
|
Ports: []string{"4646/tcp"},
|
|
Cmd: []string{"agent", "-dev"},
|
|
Env: []string{`NOMAD_LOCAL_CONFIG=bind_addr = "0.0.0.0" acl { enabled = true }`},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Could not start docker Nomad: %s", err)
|
|
}
|
|
|
|
var nomadToken string
|
|
svc, err := runner.StartService(context.Background(), func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) {
|
|
var err error
|
|
nomadapiConfig := nomadapi.DefaultConfig()
|
|
nomadapiConfig.Address = fmt.Sprintf("http://%s:%d/", host, port)
|
|
nomad, err := nomadapi.NewClient(nomadapiConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = nomad.Status().Leader()
|
|
if err != nil {
|
|
t.Logf("[DEBUG] Nomad is not ready yet: %s", err)
|
|
return nil, err
|
|
}
|
|
|
|
if bootstrap {
|
|
aclbootstrap, _, err := nomad.ACLTokens().Bootstrap(nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nomadToken = aclbootstrap.SecretID
|
|
t.Logf("[WARN] Generated Master token: %s", nomadToken)
|
|
}
|
|
|
|
nomadAuthConfig := nomadapi.DefaultConfig()
|
|
nomadAuthConfig.Address = nomad.Address()
|
|
|
|
if bootstrap {
|
|
nomadAuthConfig.SecretID = nomadToken
|
|
|
|
nomadAuth, err := nomadapi.NewClient(nomadAuthConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = preprePolicies(nomadAuth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
u, _ := docker.NewServiceURLParse(nomadapiConfig.Address)
|
|
return &Config{
|
|
ServiceURL: *u,
|
|
Token: nomadToken,
|
|
}, nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Could not start docker Nomad: %s", err)
|
|
}
|
|
|
|
return svc.Cleanup, svc.Config.(*Config)
|
|
}
|
|
|
|
func preprePolicies(nomadClient *nomadapi.Client) error {
|
|
policy := &nomadapi.ACLPolicy{
|
|
Name: "test",
|
|
Description: "test",
|
|
Rules: `namespace "default" {
|
|
policy = "read"
|
|
}
|
|
`,
|
|
}
|
|
anonPolicy := &nomadapi.ACLPolicy{
|
|
Name: "anonymous",
|
|
Description: "Deny all access for anonymous requests",
|
|
Rules: `namespace "default" {
|
|
policy = "deny"
|
|
}
|
|
agent {
|
|
policy = "deny"
|
|
}
|
|
node {
|
|
policy = "deny"
|
|
}
|
|
`,
|
|
}
|
|
|
|
_, err := nomadClient.ACLPolicies().Upsert(policy, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = nomadClient.ACLPolicies().Upsert(anonPolicy, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func TestBackend_config_Bootstrap(t *testing.T) {
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
b, err := Factory(context.Background(), config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cleanup, svccfg := prepareTestContainer(t, false)
|
|
defer cleanup()
|
|
|
|
connData := map[string]interface{}{
|
|
"address": svccfg.URL().String(),
|
|
"token": "",
|
|
}
|
|
|
|
confReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "config/access",
|
|
Storage: config.StorageView,
|
|
Data: connData,
|
|
}
|
|
|
|
resp, err := b.HandleRequest(context.Background(), confReq)
|
|
if err != nil || (resp != nil && resp.IsError()) || resp != nil {
|
|
t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err)
|
|
}
|
|
|
|
confReq.Operation = logical.ReadOperation
|
|
resp, err = b.HandleRequest(context.Background(), confReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err)
|
|
}
|
|
|
|
expected := map[string]interface{}{
|
|
"address": connData["address"].(string),
|
|
"max_token_name_length": 0,
|
|
"ca_cert": "",
|
|
"client_cert": "",
|
|
}
|
|
if !reflect.DeepEqual(expected, resp.Data) {
|
|
t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data)
|
|
}
|
|
|
|
nomadClient, err := svccfg.Client()
|
|
if err != nil {
|
|
t.Fatalf("failed to construct nomaad client, %v", err)
|
|
}
|
|
|
|
token, _, err := nomadClient.ACLTokens().Bootstrap(nil)
|
|
if err == nil {
|
|
t.Fatalf("expected acl system to be bootstrapped already, but was able to get the bootstrap token : %v", token)
|
|
}
|
|
// NOTE: fragile test, but it's the only way, AFAIK, to check that nomad is
|
|
// bootstrapped
|
|
if !strings.Contains(err.Error(), "bootstrap already done") {
|
|
t.Fatalf("expected acl system to be bootstrapped already: err: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBackend_config_access(t *testing.T) {
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
b, err := Factory(context.Background(), config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cleanup, svccfg := prepareTestContainer(t, true)
|
|
defer cleanup()
|
|
|
|
connData := map[string]interface{}{
|
|
"address": svccfg.URL().String(),
|
|
"token": svccfg.Token,
|
|
}
|
|
|
|
confReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "config/access",
|
|
Storage: config.StorageView,
|
|
Data: connData,
|
|
}
|
|
|
|
resp, err := b.HandleRequest(context.Background(), confReq)
|
|
if err != nil || (resp != nil && resp.IsError()) || resp != nil {
|
|
t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err)
|
|
}
|
|
|
|
confReq.Operation = logical.ReadOperation
|
|
resp, err = b.HandleRequest(context.Background(), confReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err)
|
|
}
|
|
|
|
expected := map[string]interface{}{
|
|
"address": connData["address"].(string),
|
|
"max_token_name_length": 0,
|
|
"ca_cert": "",
|
|
"client_cert": "",
|
|
}
|
|
if !reflect.DeepEqual(expected, resp.Data) {
|
|
t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data)
|
|
}
|
|
if resp.Data["token"] != nil {
|
|
t.Fatalf("token should not be set in the response")
|
|
}
|
|
}
|
|
|
|
func TestBackend_config_access_with_certs(t *testing.T) {
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
b, err := Factory(context.Background(), config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cleanup, svccfg := prepareTestContainer(t, true)
|
|
defer cleanup()
|
|
|
|
connData := map[string]interface{}{
|
|
"address": svccfg.URL().String(),
|
|
"token": svccfg.Token,
|
|
"ca_cert": caCert,
|
|
"client_cert": clientCert,
|
|
"client_key": clientKey,
|
|
}
|
|
|
|
confReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "config/access",
|
|
Storage: config.StorageView,
|
|
Data: connData,
|
|
}
|
|
|
|
resp, err := b.HandleRequest(context.Background(), confReq)
|
|
if err != nil || (resp != nil && resp.IsError()) || resp != nil {
|
|
t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err)
|
|
}
|
|
|
|
confReq.Operation = logical.ReadOperation
|
|
resp, err = b.HandleRequest(context.Background(), confReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err)
|
|
}
|
|
|
|
expected := map[string]interface{}{
|
|
"address": connData["address"].(string),
|
|
"max_token_name_length": 0,
|
|
"ca_cert": caCert,
|
|
"client_cert": clientCert,
|
|
}
|
|
if !reflect.DeepEqual(expected, resp.Data) {
|
|
t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data)
|
|
}
|
|
if resp.Data["token"] != nil {
|
|
t.Fatalf("token should not be set in the response")
|
|
}
|
|
}
|
|
|
|
func TestBackend_renew_revoke(t *testing.T) {
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
b, err := Factory(context.Background(), config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cleanup, svccfg := prepareTestContainer(t, true)
|
|
defer cleanup()
|
|
|
|
connData := map[string]interface{}{
|
|
"address": svccfg.URL().String(),
|
|
"token": svccfg.Token,
|
|
}
|
|
|
|
req := &logical.Request{
|
|
Storage: config.StorageView,
|
|
Operation: logical.UpdateOperation,
|
|
Path: "config/access",
|
|
Data: connData,
|
|
}
|
|
resp, err := b.HandleRequest(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
req.Path = "role/test"
|
|
req.Data = map[string]interface{}{
|
|
"policies": []string{"policy"},
|
|
"lease": "6h",
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
req.Operation = logical.ReadOperation
|
|
req.Path = "creds/test"
|
|
resp, err = b.HandleRequest(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp == nil {
|
|
t.Fatal("resp nil")
|
|
}
|
|
if resp.IsError() {
|
|
t.Fatalf("resp is error: %v", resp.Error())
|
|
}
|
|
|
|
generatedSecret := resp.Secret
|
|
generatedSecret.TTL = 6 * time.Hour
|
|
|
|
var d struct {
|
|
Token string `mapstructure:"secret_id"`
|
|
Accessor string `mapstructure:"accessor_id"`
|
|
}
|
|
if err := mapstructure.Decode(resp.Data, &d); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Logf("[WARN] Generated token: %s with accessor %s", d.Token, d.Accessor)
|
|
|
|
// Build a client and verify that the credentials work
|
|
nomadapiConfig := nomadapi.DefaultConfig()
|
|
nomadapiConfig.Address = connData["address"].(string)
|
|
nomadapiConfig.SecretID = d.Token
|
|
client, err := nomadapi.NewClient(nomadapiConfig)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
t.Log("[WARN] Verifying that the generated token works...")
|
|
_, err = client.Agent().Members, nil
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
req.Operation = logical.RenewOperation
|
|
req.Secret = generatedSecret
|
|
resp, err = b.HandleRequest(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp == nil {
|
|
t.Fatal("got nil response from renew")
|
|
}
|
|
|
|
req.Operation = logical.RevokeOperation
|
|
resp, err = b.HandleRequest(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Build a management client and verify that the token does not exist anymore
|
|
nomadmgmtConfig := nomadapi.DefaultConfig()
|
|
nomadmgmtConfig.Address = connData["address"].(string)
|
|
nomadmgmtConfig.SecretID = connData["token"].(string)
|
|
mgmtclient, err := nomadapi.NewClient(nomadmgmtConfig)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
q := &nomadapi.QueryOptions{
|
|
Namespace: "default",
|
|
}
|
|
|
|
t.Log("[WARN] Verifying that the generated token does not exist...")
|
|
_, _, err = mgmtclient.ACLTokens().Info(d.Accessor, q)
|
|
if err == nil {
|
|
t.Fatal("err: expected error")
|
|
}
|
|
}
|
|
|
|
func TestBackend_CredsCreateEnvVar(t *testing.T) {
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
b, err := Factory(context.Background(), config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cleanup, svccfg := prepareTestContainer(t, true)
|
|
defer cleanup()
|
|
|
|
req := logical.TestRequest(t, logical.UpdateOperation, "role/test")
|
|
req.Data = map[string]interface{}{
|
|
"policies": []string{"policy"},
|
|
"lease": "6h",
|
|
}
|
|
resp, err := b.HandleRequest(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
os.Setenv("NOMAD_TOKEN", svccfg.Token)
|
|
defer os.Unsetenv("NOMAD_TOKEN")
|
|
os.Setenv("NOMAD_ADDR", svccfg.URL().String())
|
|
defer os.Unsetenv("NOMAD_ADDR")
|
|
|
|
req.Operation = logical.ReadOperation
|
|
req.Path = "creds/test"
|
|
resp, err = b.HandleRequest(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp == nil {
|
|
t.Fatal("resp nil")
|
|
}
|
|
if resp.IsError() {
|
|
t.Fatalf("resp is error: %v", resp.Error())
|
|
}
|
|
}
|
|
|
|
func TestBackend_max_token_name_length(t *testing.T) {
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
b, err := Factory(context.Background(), config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cleanup, svccfg := prepareTestContainer(t, true)
|
|
defer cleanup()
|
|
|
|
testCases := []struct {
|
|
title string
|
|
roleName string
|
|
tokenLength int
|
|
}{
|
|
{
|
|
title: "Default",
|
|
},
|
|
{
|
|
title: "ConfigOverride",
|
|
tokenLength: 64,
|
|
},
|
|
{
|
|
title: "ConfigOverride-LongName",
|
|
roleName: "testlongerrolenametoexceed64charsdddddddddddddddddddddddd",
|
|
tokenLength: 64,
|
|
},
|
|
{
|
|
title: "Notrim",
|
|
roleName: "testlongersubrolenametoexceed64charsdddddddddddddddddddddddd",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.title, func(t *testing.T) {
|
|
// setup config/access
|
|
connData := map[string]interface{}{
|
|
"address": svccfg.URL().String(),
|
|
"token": svccfg.Token,
|
|
"max_token_name_length": tc.tokenLength,
|
|
}
|
|
expected := map[string]interface{}{
|
|
"address": svccfg.URL().String(),
|
|
"max_token_name_length": tc.tokenLength,
|
|
"ca_cert": "",
|
|
"client_cert": "",
|
|
}
|
|
|
|
expectedMaxTokenNameLength := maxTokenNameLength
|
|
if tc.tokenLength != 0 {
|
|
expectedMaxTokenNameLength = tc.tokenLength
|
|
}
|
|
|
|
confReq := logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "config/access",
|
|
Storage: config.StorageView,
|
|
Data: connData,
|
|
}
|
|
|
|
resp, err := b.HandleRequest(context.Background(), &confReq)
|
|
if err != nil || (resp != nil && resp.IsError()) || resp != nil {
|
|
t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err)
|
|
}
|
|
confReq.Operation = logical.ReadOperation
|
|
resp, err = b.HandleRequest(context.Background(), &confReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err)
|
|
}
|
|
|
|
// verify token length is returned in the config/access query
|
|
if !reflect.DeepEqual(expected, resp.Data) {
|
|
t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data)
|
|
}
|
|
// verify token is not returned
|
|
if resp.Data["token"] != nil {
|
|
t.Fatalf("token should not be set in the response")
|
|
}
|
|
|
|
// create a role to create nomad credentials with
|
|
// Seeds random with current timestamp
|
|
|
|
if tc.roleName == "" {
|
|
tc.roleName = "test"
|
|
}
|
|
roleTokenName := testhelpers.RandomWithPrefix(tc.roleName)
|
|
|
|
confReq.Path = "role/" + roleTokenName
|
|
confReq.Operation = logical.UpdateOperation
|
|
confReq.Data = map[string]interface{}{
|
|
"policies": []string{"policy"},
|
|
"lease": "6h",
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), &confReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
confReq.Operation = logical.ReadOperation
|
|
confReq.Path = "creds/" + roleTokenName
|
|
resp, err = b.HandleRequest(context.Background(), &confReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp == nil {
|
|
t.Fatal("resp nil")
|
|
}
|
|
if resp.IsError() {
|
|
t.Fatalf("resp is error: %v", resp.Error())
|
|
}
|
|
|
|
// extract the secret, so we can query nomad directly
|
|
generatedSecret := resp.Secret
|
|
generatedSecret.TTL = 6 * time.Hour
|
|
|
|
var d struct {
|
|
Token string `mapstructure:"secret_id"`
|
|
Accessor string `mapstructure:"accessor_id"`
|
|
}
|
|
if err := mapstructure.Decode(resp.Data, &d); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Build a client and verify that the credentials work
|
|
nomadapiConfig := nomadapi.DefaultConfig()
|
|
nomadapiConfig.Address = connData["address"].(string)
|
|
nomadapiConfig.SecretID = d.Token
|
|
client, err := nomadapi.NewClient(nomadapiConfig)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// default query options for Nomad queries ... not sure if needed
|
|
qOpts := &nomadapi.QueryOptions{
|
|
Namespace: "default",
|
|
}
|
|
|
|
// connect to Nomad and verify the token name does not exceed the
|
|
// max_token_name_length
|
|
token, _, err := client.ACLTokens().Self(qOpts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(token.Name) > expectedMaxTokenNameLength {
|
|
t.Fatalf("token name exceeds max length (%d): %s (%d)", expectedMaxTokenNameLength, token.Name, len(token.Name))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
const caCert = `-----BEGIN CERTIFICATE-----
|
|
MIIF7zCCA9egAwIBAgIINVVQic4bju8wDQYJKoZIhvcNAQELBQAwaDELMAkGA1UE
|
|
BhMCVVMxFDASBgNVBAoMC1Vuc3BlY2lmaWVkMR8wHQYDVQQLDBZjYS0zODQzMDY2
|
|
NDA5ODI5MjQwNTU5MSIwIAYDVQQDDBl4cHMxNS5sb2NhbC5jaXBoZXJib3kuY29t
|
|
MB4XDTIyMDYwMjIxMTgxN1oXDTIzMDcwNTIxMTgxN1owaDELMAkGA1UEBhMCVVMx
|
|
FDASBgNVBAoMC1Vuc3BlY2lmaWVkMR8wHQYDVQQLDBZjYS0zODQzMDY2NDA5ODI5
|
|
MjQwNTU5MSIwIAYDVQQDDBl4cHMxNS5sb2NhbC5jaXBoZXJib3kuY29tMIICIjAN
|
|
BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA35VilgfqMUKhword7wORXRFyPbpz
|
|
8uqO7eRaylMnkAkbk5eoQB/iYfXjJ6ZBs5mJGQVz5ZNvh9EzZsk1J6wqYgbwVKUx
|
|
fh4kvW6sXtDirtb4ZQAK7OTLEoapUQGnGcvm+aEYfvC1sTBl4fbex7yyN5FYMJTM
|
|
TAUumhdq2pwujaj2xkN9DwZa89Tk7tbj9HE9DTRji7bnciEtrmTAOIOfOrT/1l3x
|
|
YW1BwYXpQ0TamJ58pC/iNgEp5FAxKt9d3RggesMA7pvG/f8fNgsa/Tku/PeEXNPA
|
|
+Yx4CcAipujmqpBKiKwJ6TOzp80m2zrZ7Da4Av5vVS5GsNJxhFYD1h8hU1ptK9BS
|
|
2CaTwBpV421C9BfEmtSAksGDIWYujfiHb6XNaQrt8Hu85GBuPUudVn0lpoXLn2xD
|
|
rGK8WEK2gWZ4eez3ZDLbpLui6c1m7AVlMtj374s+LHcD7JIxY475Na7pXmEWReqM
|
|
RUyCEq1spOOn70fOdhphhmpY6DoklOTOriPawCLNmkPWRnhrIwqyP1gse9YMqQ2n
|
|
LhWUkv/08m/0pb4e5ijVhsZNzv+1PXPWCk968nzt0BMDgJT+0ZiXsaU7FILXuo7Y
|
|
Ijgrj7dpXWx2MBdMGPFQdveog7Pa80Yb7r4ERW0DL78TxYC6m/S1p14PHwZpDZzQ
|
|
LrPrBcpI5XzI7osCAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAqQwDAYDVR0TBAUw
|
|
AwEB/zA0BgNVHR4ELTAroCkwG4IZeHBzMTUubG9jYWwuY2lwaGVyYm95LmNvbTAK
|
|
hwh/AAAB/wAAADAkBgNVHREEHTAbghl4cHMxNS5sb2NhbC5jaXBoZXJib3kuY29t
|
|
MB0GA1UdDgQWBBR3bHgDp5RpzerMKRkaGDFN/ZeImjANBgkqhkiG9w0BAQsFAAOC
|
|
AgEArkuDYYWYHYxIoTeZkQz5L1y0H27ZWPJx5jBBuktPonDLQxBGAwkl6NZbJGLU
|
|
v+usII+eyjPKIgjhCiTXJAmeIngwWoN3PHOLMIPe9axuNt6qVoP4dQtzfpPR3buK
|
|
CWj9i3H0ixK73klk7QWZiBUDinYfEMSNRpU3G7NsqmqCXD4s5gB+8y9c7+zIiJyN
|
|
IaJBWpzI4eQBi/4cBhtM7Xa+CMB/8whhWYR6H+GXGZdNcP5f7bwneMstWKceTadk
|
|
IEzFucJHDySpEkIA2A9t33pV54FmEp+JVwvxAH4FABCnjPmhg0j1IonWV5pySWpG
|
|
hhEZpnRRH1XfpTA5i6dlyUA5DJjL8X1lYrgOK+LaoR52mQh5JBsMoVHFzN50DiMA
|
|
RTsbq4Qzozf23hU1BqW4NOzPTukgSGEcbT/DhXKPPPLL8JD0rPelJPq76X3TJjgZ
|
|
C9uMnZaDnxjppDXp5oBIXqC05FDxJ5sSODNOpKGyuzOU2qQLMau33yYOgaSAttBk
|
|
r29+LNFJ+0QzMuPjYXPznpxbsI+lrlZ3F2tDGGs8+JVceC1YX+cBEsEOiqNGTIip
|
|
/DY3b9gu5oiTwhcFyQW8+WFsirRS/g5t+M40WLKVPdK09z96krFXQMkL6a7LHLY1
|
|
n9ivwj+sTG1XmJYXp8naLg4wdzIUf2fJxaFNI5Yq4elZ8sY=
|
|
-----END CERTIFICATE-----`
|
|
|
|
const clientCert = `-----BEGIN CERTIFICATE-----
|
|
MIIEsDCCApigAwIBAgIIRY1JBRIynFYwDQYJKoZIhvcNAQELBQAwaDELMAkGA1UE
|
|
BhMCVVMxFDASBgNVBAoMC1Vuc3BlY2lmaWVkMR8wHQYDVQQLDBZjYS0zODQzMDY2
|
|
NDA5ODI5MjQwNTU5MSIwIAYDVQQDDBl4cHMxNS5sb2NhbC5jaXBoZXJib3kuY29t
|
|
MB4XDTIyMDYwMjIxMTgxOFoXDTIzMDcwNTIxMTgxOFowRzELMAkGA1UEBhMCVVMx
|
|
FDASBgNVBAoMC1Vuc3BlY2lmaWVkMSIwIAYDVQQDDBl4cHMxNS5sb2NhbC5jaXBo
|
|
ZXJib3kuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs+XYhsW2
|
|
vTwN7gY3xMxgbNN8d3aoeqCswOp05BBf0Vgv3febahm422ubXXd5Mg2UGiU7sJVe
|
|
4tUpDeupVVRX5Qr/hpiXgEyfRDAAAJKqrl65KSS62TCbT/eJZ0ah25HV1evI4uM2
|
|
0kl5QWhtQjDyaVlTS38YFqXXQvpOuU5DG6UbKnpMcpsCPTyUKEJvJ95ZLcz0HJ8I
|
|
kIHrnX0Lt0pOhkllj5Nk4cXhU8CFk8IGNz7SVAycrUsffAUMNNEbrIOIfOTPHR1c
|
|
q3X9hO4/5pt80uIDMFwwumoA7nQR0AhlKkw9SskCIzJhKwKwssQY7fmovNG0fOEd
|
|
/+vSHK7OsYW+gwIDAQABo38wfTAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYI
|
|
KwYBBQUHAwIwCQYDVR0TBAIwADAqBgNVHREEIzAhghl4cHMxNS5sb2NhbC5jaXBo
|
|
ZXJib3kuY29thwR/AAABMB8GA1UdIwQYMBaAFHdseAOnlGnN6swpGRoYMU39l4ia
|
|
MA0GCSqGSIb3DQEBCwUAA4ICAQBUSP4ZJglCCrYkM5Le7McdvfkM5uYv1aQn0sM4
|
|
gbyDEWO0fnv50vLpD3y4ckgHLoD52pAZ0hN8a7rwAUae21GA6DvEchSH5x/yvJiS
|
|
7FBlq39sAafe03ZlzDErNYJRkLcnPAqG74lJ1SSsMcs9gCPHM8R7HtNnhAga06L7
|
|
K8/G43dsGZCmEb+xcX2B9McCt8jBG6TJPTGafb3BJ0JTmR/tHdoLFIiNwI+qzd2U
|
|
lMnGlkIApULX8tmIMsWO0rjdiFkPWGcmfn9ChC0iDpQOAcKSDBcZlWrDNpzKk0mK
|
|
l0TbE6cxcmCUUpiwaXFrbkwVWQw4W0c4b3sWFtWifFbiR1qZ/OT2Y2sHbkbxwvPl
|
|
PjjXMDBAdRRwtNcTP1E55I5zvwzzBxUpxOob0miorhTJrZR9So0rgv7Roce4ED6M
|
|
WETYa/mGhe+Q7gBQygIVoryfQLgGBsHC+7V4RDvYTazwZkz9nLQxHLI/TAZU5ofM
|
|
WqdoUkMd68rxTTEUoMfGbftxjKA0raxGcO7/PjLR3O743EwCqeqYJ7OKWgGRLnui
|
|
kIKNUJlZ9umURUFzL++Bx4Pr95jWXb2WYqYYQxhDz0oR5q5smnFm5+/1/MLDMvDU
|
|
TrgBK6pey4QF33B/I55H1+7tGdv85Q57Z8UrNi/IQxR2sFlsOTeCwStpBQ56sdZk
|
|
Wi4+cQ==
|
|
-----END CERTIFICATE-----`
|
|
|
|
const clientKey = `-----BEGIN PRIVATE KEY-----
|
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCz5diGxba9PA3u
|
|
BjfEzGBs03x3dqh6oKzA6nTkEF/RWC/d95tqGbjba5tdd3kyDZQaJTuwlV7i1SkN
|
|
66lVVFflCv+GmJeATJ9EMAAAkqquXrkpJLrZMJtP94lnRqHbkdXV68ji4zbSSXlB
|
|
aG1CMPJpWVNLfxgWpddC+k65TkMbpRsqekxymwI9PJQoQm8n3lktzPQcnwiQgeud
|
|
fQu3Sk6GSWWPk2ThxeFTwIWTwgY3PtJUDJytSx98BQw00Rusg4h85M8dHVyrdf2E
|
|
7j/mm3zS4gMwXDC6agDudBHQCGUqTD1KyQIjMmErArCyxBjt+ai80bR84R3/69Ic
|
|
rs6xhb6DAgMBAAECggEAPBcja2kxcCZWNNKo4DiwYMmHwtPE1SlEazAlmWSKzP+b
|
|
BZbGt/sdj1VzURYuSnTUqqMTPBm41yYCj57PMix5K42v6sKfoIB3lqw94/MZxiLn
|
|
0IFvVErzJhP2NqQWPqSI++rFcFwbHMTkFuAN1tVIs73dn9M1NaNxsvKvRyCIM/wz
|
|
5YQSDyTkdW4jQM2RvUFOoqwmeyAlQoBRMgQ4bHfLHxmPEjFgw1MAmmG8bJdkupin
|
|
MVzhZyKj4Fh80Xa2MU4KokijjG41hmYbg/sjNHaHJFDA92Rwq13dhWytrauJDxa/
|
|
3yj8pHWc23Y3hXvRAf/cibDVzXmmLj49W1i06KuUCQKBgQDj5yF/DJV0IOkhfbol
|
|
+f5AGH4ZrEXA/JwA5SxHU+aKhUuPEqK/LeUWqiy3szFjOz2JOnCC0LMN42nsmMyK
|
|
sdQEKHp2SPd2wCxsAKZAuxrEi6yBt1mEPFFU5yzvZbdMqYChKJjm9fbRHtuc63s8
|
|
PyVw67Ii9o4ij+PxfTobIs18xwKBgQDKE59w3uUDt2uoqNC8x4m5onL2p2vtcTHC
|
|
CxU57mu1+9CRM8N2BEp2VI5JaXjqt6W4u9ISrmOqmsPgTwosAquKpA/nu3bVvR9g
|
|
WlN9dh2Xgza0/AFaA9CB++ier8RJq5xFlcasMUmgkhYt3zgKNgRDfjfREWM0yamm
|
|
P++hAYRcZQKBgHEuYQk6k6J3ka/rQ54GmEj2oPFZB88+5K7hIWtO9IhIiGzGYYK2
|
|
ZTYrT0fvuxA/5GCZYDTnNnUoQnuYqsQaamOiQqcpt5QG/kiozegJw9JmV0aYauFs
|
|
HyweHsfJaQ2uhE4E3mKdNnVGcORuYeZaqdp5gx8v+QibEyXj/g5p60kTAoGBALKp
|
|
TMOHXmW9yqKwtvThWoRU+13WQlcJSFvuXpL8mCCrBgkLAhqaypb6RV7ksLKdMhk1
|
|
fhNkOdxBv0LXvv+QUMhgK2vP084/yrjuw3hecOVfboPvduZ2DuiNp2p9rocQAjeH
|
|
p8LgRN+Bqbhe7fYhMf3WX1UqEVM/pQ3G43+vjq39AoGAOyD2/hFSIx6BMddUNTHG
|
|
BEsMUc/DHYslZebbF1zAWnkKdTt+URhtHAFB2tYRDgkZfwW+wr/w12dJTIkX965o
|
|
HO7tI4FgpU9b0i8FTuwYkBfjwp2j0Xd2/VBR8Qpd17qKl3I6NXDsf3ykjGZAvldH
|
|
Tll+qwEZpXSRa5OWWTpGV8I=
|
|
-----END PRIVATE KEY-----`
|