diff --git a/test/e2e/internal/notation/host.go b/test/e2e/internal/notation/host.go index af7657585..613c0705e 100644 --- a/test/e2e/internal/notation/host.go +++ b/test/e2e/internal/notation/host.go @@ -5,6 +5,7 @@ import ( "path/filepath" "github.com/notaryproject/notation/test/e2e/internal/utils" + . "github.com/onsi/ginkgo/v2" ) // CoreTestFunc is the test function running in a VirtualHost. @@ -40,6 +41,17 @@ func Host(options []utils.HostOption, fn CoreTestFunc) { fn(vhost.Executor, artifact, vhost) } +// HostInGithubAction only run the test in GitHub Actions. +// +// The booting script will setup TLS reverse proxy and TLS certificate +// for Github Actions environment. +func HostInGithubAction(options []utils.HostOption, fn CoreTestFunc) { + if os.Getenv("GITHUB_ACTIONS") != "true" { + Skip("only run in GitHub Actions") + } + Host(options, fn) +} + // GeneralHost creates a virtualized notation testing host by modify // the "XDG_CONFIG_HOME" environment variable of the Executor. It's agnostic to // the Repository of the artifact. diff --git a/test/e2e/internal/notation/init.go b/test/e2e/internal/notation/init.go index 0615cb806..c6c2a1292 100644 --- a/test/e2e/internal/notation/init.go +++ b/test/e2e/internal/notation/init.go @@ -23,6 +23,7 @@ const ( envKeyRegistryHost = "NOTATION_E2E_REGISTRY_HOST" envKeyRegistryUsername = "NOTATION_E2E_REGISTRY_USERNAME" envKeyRegistryPassword = "NOTATION_E2E_REGISTRY_PASSWORD" + envKeyDomainRegistryHost = "NOTATION_E2E_DOMAIN_REGISTRY_HOST" envKeyNotationBinPath = "NOTATION_E2E_BINARY_PATH" envKeyNotationOldBinPath = "NOTATION_E2E_OLD_BINARY_PATH" envKeyNotationPluginPath = "NOTATION_E2E_PLUGIN_PATH" @@ -65,6 +66,7 @@ func setUpRegistry() { setValue(envKeyRegistryHost, &TestRegistry.Host) setValue(envKeyRegistryUsername, &TestRegistry.Username) setValue(envKeyRegistryPassword, &TestRegistry.Password) + setValue(envKeyDomainRegistryHost, &TestRegistry.DomainHost) setPathValue(envKeyOCILayoutPath, &OCILayoutPath) setValue(envKeyTestRepo, &TestRepoUri) diff --git a/test/e2e/internal/notation/registry.go b/test/e2e/internal/notation/registry.go index 89722bd04..8b8c363d2 100644 --- a/test/e2e/internal/notation/registry.go +++ b/test/e2e/internal/notation/registry.go @@ -19,9 +19,19 @@ import ( const ArtifactTypeNotation = "application/vnd.cncf.notary.signature" type Registry struct { - Host string + // Host is the registry host. + Host string + // Username is the username to access the registry. Username string + // Password is the password to access the registry. Password string + // DomainHost is a registry host, separate from localhost, used for testing + // the --insecure-registry flag. + // + // If the host is localhost, Notation connects via plain HTTP. For + // non-localhost hosts, Notation defaults to HTTPS. However, users can + // enforce HTTP by setting the --insecure-registry flag. + DomainHost string } // CreateArtifact copies a local OCI layout to the registry to create @@ -101,6 +111,12 @@ func (r *Artifact) ReferenceWithDigest() string { return fmt.Sprintf("%s/%s@%s", r.Host, r.Repo, r.Digest) } +// DomainReferenceWithDigest returns the /@: +// for testing --insecure-registry flag and TLS request. +func (r *Artifact) DomainReferenceWithDigest() string { + return fmt.Sprintf("%s/%s@%s", r.DomainHost, r.Repo, r.Digest) +} + // SignatureManifest returns the manifest of the artifact. func (r *Artifact) SignatureDescriptors() ([]ocispec.Descriptor, error) { ctx := context.Background() diff --git a/test/e2e/internal/utils/matcher.go b/test/e2e/internal/utils/matcher.go index 103d8d9dc..a7a9e3855 100644 --- a/test/e2e/internal/utils/matcher.go +++ b/test/e2e/internal/utils/matcher.go @@ -5,9 +5,15 @@ import ( "strings" . "github.com/onsi/gomega" + "github.com/onsi/gomega/format" "github.com/onsi/gomega/gexec" ) +func init() { + // expand the length limit for the gomega matcher + format.MaxLength = 1000000 +} + // Matcher contains the execution result for matching. type Matcher struct { Session *gexec.Session @@ -48,6 +54,15 @@ func (m *Matcher) MatchErrKeyWords(keywords ...string) *Matcher { return m } +// NoMatchErrKeyWords guarantees that the given keywords do not match with +// the stderr. +func (m *Matcher) NoMatchErrKeyWords(keywords ...string) *Matcher { + for _, w := range keywords { + Expect(m.stderr).ShouldNot(ContainSubstring(w)) + } + return m +} + // MatchErrKeyWords matches given keywords with the stderr. func matchKeyWords(content string, keywords []string) { var missed []string diff --git a/test/e2e/run.sh b/test/e2e/run.sh index 4d9eb861e..55ad071c8 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -40,7 +40,7 @@ if [ ! -f "$NOTATION_E2E_OLD_BINARY_PATH" ]; then echo "Try to use old notation binary at $NOTATION_E2E_OLD_BINARY_PATH" if [ ! -f $NOTATION_E2E_OLD_BINARY_PATH ]; then - TAG=1.0.0-rc.2 # without 'v' + TAG=1.0.0-rc.5 # without 'v' echo "Didn't find old notation binary locally. Try to download notation v$TAG." TAR_NAME=notation_${TAG}_linux_amd64.tar.gz diff --git a/test/e2e/scripts/dockerhub.sh b/test/e2e/scripts/dockerhub.sh index 6a0814678..1e62c4206 100644 --- a/test/e2e/scripts/dockerhub.sh +++ b/test/e2e/scripts/dockerhub.sh @@ -16,9 +16,10 @@ if [ -z "$DOCKER_USERNAME" ] || [ -z "$DOCKER_PASSWORD" ]; then fi # set environment variables for E2E testing -export NOTATION_E2E_REGISTRY_HOST=docker.io/$DOCKER_USERNAME -export NOTATION_E2E_REGISTRY_USERNAME=$DOCKER_USERNAME -export NOTATION_E2E_REGISTRY_PASSWORD=$DOCKER_PASSWORD +export NOTATION_E2E_REGISTRY_HOST="docker.io/$DOCKER_USERNAME" +export NOTATION_E2E_REGISTRY_USERNAME="$DOCKER_USERNAME" +export NOTATION_E2E_REGISTRY_PASSWORD="$DOCKER_PASSWORD" +export NOTATION_E2E_DOMAIN_REGISTRY_HOST="$NOTATION_E2E_REGISTRY_HOST" function setup_registry { echo "use $NOTATION_E2E_REGISTRY_HOST" @@ -55,4 +56,4 @@ function cleanup_registry { echo "$NOTATION_E2E_REGISTRY_HOST/$repoName deleted." done done -} \ No newline at end of file +} diff --git a/test/e2e/scripts/tls.sh b/test/e2e/scripts/tls.sh new file mode 100644 index 000000000..e12795a48 --- /dev/null +++ b/test/e2e/scripts/tls.sh @@ -0,0 +1,51 @@ +#!/bin/bash -e +# +# Usage +# For setup: +# 1. source ./scripts/tls.sh +# 2. call create_docker_network +# 3. setup registry with port 5000 in $DOCKER_NETWORK +# 4. call setup_tls reverse proxy +# +# For clean up: +# 1. call clean_up +# 2. clean up registry +# 3. call remove_docker_network +# +# Note: this script needs sudo permission to add TLS certificate to system and +# add domain registry host. + +NGINX_CONTAINER_NAME=nginx +DOMAIN=notation-e2e.registry.io +DOCKER_NETWORK=notation-e2e + +function create_docker_network { + docker network create "$DOCKER_NETWORK" +} + +function remove_docker_network { + docker network rm "$DOCKER_NETWORK" +} + +function setup_tls { + # add domain registry host to /etc/hosts for testing --plain-http feature + echo "127.0.0.1 $DOMAIN" | sudo tee -a /etc/hosts + # add TLS certificate to system + sudo mkdir -p /usr/local/share/ca-certificates/ + sudo cp ./testdata/nginx/notation-e2e.registry.io.crt /usr/local/share/ca-certificates/ + sudo update-ca-certificates + + # start Nginx for TLS + docker run -d -p 80:80 -p 443:443 \ + --network "$DOCKER_NETWORK" \ + --mount type=bind,source="$(pwd)/testdata/nginx/",target=/etc/nginx \ + --name "$NGINX_CONTAINER_NAME" \ + --rm nginx:latest +} + +function clean_up_tls { + docker container stop "$NGINX_CONTAINER_NAME" 1>/dev/null && echo "Nginx stopped" + sudo sed -i "/${NOTATION_E2E_DOMAIN_REGISTRY_HOST}/d" /etc/hosts + sudo rm /usr/local/share/ca-certificates/notation-e2e.registry.io.crt + sudo update-ca-certificates +} diff --git a/test/e2e/scripts/zot.sh b/test/e2e/scripts/zot.sh index ebd4f7cd2..4941c4b43 100644 --- a/test/e2e/scripts/zot.sh +++ b/test/e2e/scripts/zot.sh @@ -4,24 +4,38 @@ # Usage # ./run.sh zot [old-notation-binary-path] +source ./scripts/tls.sh + REG_HOST=localhost REG_PORT=5000 -ZOT_CONTAINER_NAME=zot +ZOT_CONTAINER_NAME=notation-e2e-registry -# set environment variables for E2E testing -export NOTATION_E2E_REGISTRY_HOST=$REG_HOST:$REG_PORT +# set required environment variables for E2E testing +export NOTATION_E2E_REGISTRY_HOST="$REG_HOST:$REG_PORT" export NOTATION_E2E_REGISTRY_USERNAME=testuser export NOTATION_E2E_REGISTRY_PASSWORD=testpassword +export NOTATION_E2E_DOMAIN_REGISTRY_HOST="$DOMAIN" function setup_registry { + create_docker_network # start Zot - docker run -d -p $REG_PORT:$REG_PORT -it --name $ZOT_CONTAINER_NAME \ - --mount type=bind,source=`pwd`/testdata/registry/zot/,target=/etc/zot \ + docker run -d -p $REG_PORT:$REG_PORT -it \ + --name $ZOT_CONTAINER_NAME \ + --network $DOCKER_NETWORK \ + --mount type=bind,source="$(pwd)/testdata/registry/zot/",target=/etc/zot \ --rm ghcr.io/project-zot/zot-minimal-linux-amd64:latest + + if [ "$GITHUB_ACTIONS" == "true" ]; then + setup_tls + fi # make sure that Zot is ready sleep 1 } function cleanup_registry { docker container stop $ZOT_CONTAINER_NAME 1>/dev/null && echo "Zot stopped" -} \ No newline at end of file + if [ "$GITHUB_ACTIONS" == "true" ]; then + clean_up_tls + fi + remove_docker_network +} diff --git a/test/e2e/suite/command/inspect.go b/test/e2e/suite/command/inspect.go new file mode 100644 index 000000000..7cf6d722f --- /dev/null +++ b/test/e2e/suite/command/inspect.go @@ -0,0 +1,67 @@ +package command + +import ( + . "github.com/notaryproject/notation/test/e2e/internal/notation" + "github.com/notaryproject/notation/test/e2e/internal/utils" + . "github.com/notaryproject/notation/test/e2e/suite/common" + . "github.com/onsi/ginkgo/v2" +) + +var inspectSuccessfully = []string{ + "└── application/vnd.cncf.notary.signature", + "└── sha256:", + "├── media type:", + "├── signature algorithm:", + "├── signed attributes", + "signingTime:", + "signingScheme:", + "├── user defined attributes", + "│ └── (empty)", + "├── unsigned attributes", + "│ └── signingAgent: Notation/", + "├── certificates", + "│ └── SHA256 fingerprint:", + "issued to:", + "issued by:", + "expiry:", + "└── signed artifact", + "media type:", + "digest:", + "size:", +} + +var _ = Describe("notation inspect", func() { + It("all signatures of an image", func() { + Host(BaseOptions(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("sign", artifact.ReferenceWithDigest()). + MatchKeyWords(SignSuccessfully) + + notation.Exec("inspect", "-d", artifact.ReferenceWithDigest()). + MatchKeyWords(inspectSuccessfully...) + }) + }) + + It("all signatures of an image with TLS", func() { + HostInGithubAction(BaseOptions(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("sign", artifact.DomainReferenceWithDigest()). + MatchKeyWords(SignSuccessfully) + + notation.Exec("inspect", "-d", artifact.DomainReferenceWithDigest()). + MatchKeyWords(inspectSuccessfully...). + MatchErrKeyWords("https://notation-e2e.registry.io/v2/e2e"). + NoMatchErrKeyWords("http://notation-e2e.registry.io") + }) + }) + + It("all signatures of an image with --insecure-registry flag", func() { + HostInGithubAction(BaseOptions(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("sign", artifact.DomainReferenceWithDigest()). + MatchKeyWords(SignSuccessfully) + + notation.Exec("inspect", "-d", "--insecure-registry", artifact.DomainReferenceWithDigest()). + MatchKeyWords(inspectSuccessfully...). + MatchErrKeyWords(HTTPRequest). + NoMatchErrKeyWords(HTTPSRequest) + }) + }) +}) diff --git a/test/e2e/suite/command/list.go b/test/e2e/suite/command/list.go new file mode 100644 index 000000000..c04d039ef --- /dev/null +++ b/test/e2e/suite/command/list.go @@ -0,0 +1,53 @@ +package command + +import ( + . "github.com/notaryproject/notation/test/e2e/internal/notation" + "github.com/notaryproject/notation/test/e2e/internal/utils" + . "github.com/notaryproject/notation/test/e2e/suite/common" + . "github.com/onsi/ginkgo/v2" +) + +var _ = Describe("notation list", func() { + It("all signatures of an image", func() { + Host(BaseOptions(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("sign", artifact.ReferenceWithDigest()). + MatchKeyWords(SignSuccessfully) + + notation.Exec("list", "-d", artifact.ReferenceWithDigest()). + MatchKeyWords( + "└── application/vnd.cncf.notary.signature", + "└── sha256:", + ) + }) + }) + + It("all signatures of an image with TLS", func() { + HostInGithubAction(BaseOptions(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("sign", artifact.DomainReferenceWithDigest()). + MatchKeyWords(SignSuccessfully) + + notation.Exec("list", "-d", artifact.DomainReferenceWithDigest()). + MatchKeyWords( + "└── application/vnd.cncf.notary.signature", + "└── sha256:", + ). + MatchErrKeyWords("https://notation-e2e.registry.io/v2/e2e"). + NoMatchErrKeyWords("http://notation-e2e.registry.io") + }) + }) + + It("all signatures of an image with --insecure-registry flag", func() { + HostInGithubAction(BaseOptions(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("sign", artifact.DomainReferenceWithDigest()). + MatchKeyWords(SignSuccessfully) + + notation.Exec("list", "-d", "--insecure-registry", artifact.DomainReferenceWithDigest()). + MatchKeyWords( + "└── application/vnd.cncf.notary.signature", + "└── sha256:", + ). + MatchErrKeyWords(HTTPRequest). + NoMatchErrKeyWords(HTTPSRequest) + }) + }) +}) diff --git a/test/e2e/suite/command/sign.go b/test/e2e/suite/command/sign.go index 67a75df6c..8181fc9f8 100644 --- a/test/e2e/suite/command/sign.go +++ b/test/e2e/suite/command/sign.go @@ -150,4 +150,28 @@ var _ = Describe("notation sign", func() { MatchErrContent(expectedErrMsg) }) }) + + It("with TLS by digest", func() { + HostInGithubAction(BaseOptions(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("sign", "-d", artifact.DomainReferenceWithDigest()). + MatchKeyWords(SignSuccessfully). + MatchErrKeyWords(HTTPSRequest). + NoMatchErrKeyWords(HTTPRequest) + + OldNotation().Exec("verify", artifact.DomainReferenceWithDigest()). + MatchKeyWords(VerifySuccessfully) + }) + }) + + It("with --insecure-registry by digest", func() { + HostInGithubAction(BaseOptions(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("sign", "-d", "--insecure-registry", artifact.DomainReferenceWithDigest()). + MatchKeyWords(SignSuccessfully). + MatchErrKeyWords(HTTPRequest). + NoMatchErrKeyWords(HTTPSRequest) + + OldNotation().Exec("verify", artifact.DomainReferenceWithDigest()). + MatchKeyWords(VerifySuccessfully) + }) + }) }) diff --git a/test/e2e/suite/command/verify.go b/test/e2e/suite/command/verify.go index 6abf8dc43..0056ed651 100644 --- a/test/e2e/suite/command/verify.go +++ b/test/e2e/suite/command/verify.go @@ -121,4 +121,32 @@ var _ = Describe("notation verify", func() { MatchErrKeyWords(expectedErrMsg) }) }) + + It("with TLS by digest", func() { + HostInGithubAction(BaseOptions(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("sign", artifact.DomainReferenceWithDigest()). + MatchKeyWords(SignSuccessfully) + + notation.Exec("verify", "-d", artifact.DomainReferenceWithDigest()). + MatchKeyWords( + VerifySuccessfully, + ). + MatchErrKeyWords("https://notation-e2e.registry.io/v2/e2e"). + NoMatchErrKeyWords("http://notation-e2e.registry.io") + }) + }) + + It("with --insecure-registry by digest", func() { + HostInGithubAction(BaseOptions(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("sign", artifact.DomainReferenceWithDigest()). + MatchKeyWords(SignSuccessfully) + + notation.Exec("verify", "-d", "--insecure-registry", artifact.DomainReferenceWithDigest()). + MatchKeyWords( + VerifySuccessfully, + ). + MatchErrKeyWords(HTTPRequest). + NoMatchErrKeyWords(HTTPSRequest) + }) + }) }) diff --git a/test/e2e/suite/common/common.go b/test/e2e/suite/common/common.go index 5d3109a86..c14d426a9 100644 --- a/test/e2e/suite/common/common.go +++ b/test/e2e/suite/common/common.go @@ -1,5 +1,11 @@ package common +import ( + "fmt" + + . "github.com/notaryproject/notation/test/e2e/internal/notation" +) + const ( LoginSuccessfully = "Login Succeeded" LogoutSuccessfully = "Logout Succeeded" @@ -8,6 +14,15 @@ const ( VerifyFailed = "signature verification failed" ) +var ( + // HTTPRequest is the base URL for HTTP requests for testing + // --insecure-registry flag + HTTPRequest = fmt.Sprintf("http://%s", TestRegistry.DomainHost) + + // HTTPSRequest is the base URL for HTTPS requests for testing TLS request. + HTTPSRequest = fmt.Sprintf("https://%s", TestRegistry.DomainHost) +) + const ( // HeaderVerificationPlugin specifies the name of the verification plugin that should be used to verify the signature. HeaderVerificationPlugin = "io.cncf.notary.verificationPlugin" diff --git a/test/e2e/testdata/nginx/nginx.conf b/test/e2e/testdata/nginx/nginx.conf new file mode 100644 index 000000000..9e4ee853c --- /dev/null +++ b/test/e2e/testdata/nginx/nginx.conf @@ -0,0 +1,19 @@ +events { + worker_connections 1024; +} + +http { + server { + listen 80; + listen 443 ssl; + + server_name notation-e2e.regisry.io; + + ssl_certificate /etc/nginx/notation-e2e.registry.io.crt; + ssl_certificate_key /etc/nginx/notation-e2e.registry.io.key; + + location / { + proxy_pass http://notation-e2e-registry:5000; + } + } +} diff --git a/test/e2e/testdata/nginx/notation-e2e.registry.io.crt b/test/e2e/testdata/nginx/notation-e2e.registry.io.crt new file mode 100644 index 000000000..a54faeebf --- /dev/null +++ b/test/e2e/testdata/nginx/notation-e2e.registry.io.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDHDCCAgSgAwIBAgIUQcDBs3e5Z0lR1hUqQZs0diP7CQAwDQYJKoZIhvcNAQEL +BQAwIzEhMB8GA1UEAwwYbm90YXRpb24tZTJlLnJlZ2lzdHJ5LmlvMCAXDTIzMDUy +MjA5MDAyMFoYDzIxMjMwNDI4MDkwMDIwWjAjMSEwHwYDVQQDDBhub3RhdGlvbi1l +MmUucmVnaXN0cnkuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDn +LxBVN7iawG8Q5MD1Fczv0Zr1QG5pRnz5DXo2hLSnrJ2xYiXWRgIlfI67adMPiF3v +LZXKVJtVkkYAL/0iLu6YpIHltmzXo+/rY3RV+jk0Lj380Zfp5gC6SLXIKuzM9AnT +g3pkOt7zBHQP0xOcK2aPuhPEySuSoGQ6jupWFD3vPBgvcW7+sF0NUHdTnN6dz0sR +dmlbEDmaYJ+weZa0Skvc1Mc2znnJmWZWY6PlC7SPQey6MC5CTjukT8AVMqwedjfv +fQ8vOhvxu+UFnncEqwM7f83hiYD4QWK/ZA5iIobpd0aP/83R87WkGNyHypnWbmEb +C7Py8OWZoFKqcBHj13c/AgMBAAGjRjBEMCMGA1UdEQQcMBqCGG5vdGF0aW9uLWUy +ZS5yZWdpc3RyeS5pbzAdBgNVHQ4EFgQUCNOgP6vTy0+IO3UA1mZ9G8pAHqQwDQYJ +KoZIhvcNAQELBQADggEBAIofq+60DleGTdxr4RqvaXytIbNX57TrAjUvFwFy9E3e +mxmy14vHkwOLRoCMbbP4wy+XG2I1r7NcmhDxGc9z9z+YrhuW9IDWSxv8+KYWM5T/ +7viqHJtyYk5eMpN+k+7qB8kXoahnrgOZjmdDwJBsj6kwrcnuqvwRBi3fxeNJl+/Q +tDOEPyqIybWMw43tTvE33k0k8ia7nEk0slcURdLVllAfTC6DdL2SJtELD8SMvPPh +nwYKU6dGbYlnWVRrfqQG3PCksuLbRzX+zQCAMlSgtD+KSpjyxkaazPY/SD4gLav7 +u+96xpZVLbrsjklcrT2dEku3zprnM63P8DnV/wHDdUc= +-----END CERTIFICATE----- diff --git a/test/e2e/testdata/nginx/notation-e2e.registry.io.key b/test/e2e/testdata/nginx/notation-e2e.registry.io.key new file mode 100644 index 000000000..44000441b --- /dev/null +++ b/test/e2e/testdata/nginx/notation-e2e.registry.io.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDnLxBVN7iawG8Q +5MD1Fczv0Zr1QG5pRnz5DXo2hLSnrJ2xYiXWRgIlfI67adMPiF3vLZXKVJtVkkYA +L/0iLu6YpIHltmzXo+/rY3RV+jk0Lj380Zfp5gC6SLXIKuzM9AnTg3pkOt7zBHQP +0xOcK2aPuhPEySuSoGQ6jupWFD3vPBgvcW7+sF0NUHdTnN6dz0sRdmlbEDmaYJ+w +eZa0Skvc1Mc2znnJmWZWY6PlC7SPQey6MC5CTjukT8AVMqwedjfvfQ8vOhvxu+UF +nncEqwM7f83hiYD4QWK/ZA5iIobpd0aP/83R87WkGNyHypnWbmEbC7Py8OWZoFKq +cBHj13c/AgMBAAECggEAQhYnnpy/pmlVNqiV6lnRjErId8xz459VUWLDaXtVI0uK +hq8ubsrziSDKspOFVL7gT2OiGsVF5Ffcr+gH/jIZXcRFJ9QW2CwShSEYnA1cNej0 +KmYF/cSUt6vaXz66E7q9ZlwC7E0R97lxriZiSDX16yc/yHTTgmZcUIsTPQkrTUw+ +7IgY0uV3HFCnGU5i5jeB+UF5MIPauBPt3YQcbBythwkZadxaKI/1Mp1vn4RcdzMg ++XJ4GMND1A1qzn3mQm5PqBMk4NC+FVcBxvdxzZum7qu1iLap0e3fyD9PqFTb1Zix +3976Qj/QrttbO1WXDtBvEaXqlGVbEDR1cpKQKYghwQKBgQDx+GkdBb6lYGDERnZC +pY65s0k2HVnZ6BxFGJXMryWN8CYMXKgns0O4idIYQQnzWFUmudxfAy13H3wukXDC +aHF2YZ1QuqOmc3kIkxKVD+9QfcgKGfLAGemyFtwhSbegk6MKF4UOqDNL9unvDdAo +7j9rjq5IOqfgPorqm1AoySgDQQKBgQD0loxIEM/3O5PqT8sda5G9bsXh9QlV3Za0 +XSVYlkKxXuPIlC0jmDHk/xI41ykdrqjwmYd2FOt7Luuevke/ahqQSbXxy0LFdR5J +QJ6iN3OMZMsXMImRCoLD0kEvh+Z1u4VUiHuXmBQtiv5uP9S1KAETL44SEZyfJn63 +uvNMlplafwKBgENb58cQhlX7UnTROLKs6+J+Km9KFG041EXX5juotkehBraCRL1o +hf2lQDtIP8DiYjH5o4M/mzSCK0u7aSx1bsCJxAVpL41yr8rXRmEAoppBqaJGPvGD +RS8yde0+XEPzVXvFuGCwKjeHcO//ZGdAi58hhRrOWVVvk7RjsBjqhp0BAoGBAJv5 +WZInbofKLYSRyASF8ZWtC3IR8hcYzR9N+x/oCrXTvkzN+Y8mYkMXSkaHJ0gvdrqg +HZt2sciHXmiIDXcKsc/rwaRlK7qB+oNaOw9Vb1FLgZvTLxcYbdV0wm8OKjBQGjGT +K8W7jLqSVbh26i1wSmcyv1XUd12ijdKa3MatjzP/AoGARuw3wa+b0GCH5jtzJMBs +9N/Tr9lGLHH7Q57c4nAR2rR5L0DGAns5XWN9yKCyIDFFh89kLGNRwj/cONY3nXud +/1l7seup811NusPiBaZC+5LX/zAvzH0orFzinEdV7V4g435j/i52V5Bl5fJVw7FM +CfqmotJm0hceUk1QaStn+Ds= +-----END PRIVATE KEY-----