From 0ef7251e1f1640611a9c237b7da7095312e715ce Mon Sep 17 00:00:00 2001 From: Matt Curtis Date: Fri, 7 Jun 2024 11:46:20 +0100 Subject: [PATCH] EDB Connect: Add db query tool --- .../workflows/manual-deploy-testnet-l2.yml | 3 + .../workflows/manual-publish-edb-connect.yml | 33 ++++ .../storage/init/edgelessdb/edgelessdb.go | 27 +-- integration/common/testlog/testlog.go | 9 + .../networktest/tests/helpful/smoke_test.go | 2 +- tools/edbconnect/Dockerfile | 41 ++++ tools/edbconnect/edb-connect.sh | 43 ++++ tools/edbconnect/main/edb-enclave.json | 19 ++ tools/edbconnect/main/main.go | 185 ++++++++++++++++++ tools/edbconnect/main/testnet.pem | 39 ++++ 10 files changed, 387 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/manual-publish-edb-connect.yml create mode 100644 tools/edbconnect/Dockerfile create mode 100644 tools/edbconnect/edb-connect.sh create mode 100644 tools/edbconnect/main/edb-enclave.json create mode 100644 tools/edbconnect/main/main.go create mode 100644 tools/edbconnect/main/testnet.pem diff --git a/.github/workflows/manual-deploy-testnet-l2.yml b/.github/workflows/manual-deploy-testnet-l2.yml index 7b54c25cd1..060e89cb87 100644 --- a/.github/workflows/manual-deploy-testnet-l2.yml +++ b/.github/workflows/manual-deploy-testnet-l2.yml @@ -224,6 +224,9 @@ jobs: --command-id RunShellScript \ --scripts 'mkdir -p /home/obscuro \ && git clone --depth 1 -b ${{ env.BRANCH_NAME }} https://github.com/ten-protocol/go-ten.git /home/obscuro/go-obscuro \ + && cp /home/obscuro/go-obscuro/tools/edbconnect/edb-connect.sh /home/obscurouser/edb-connect.sh \ + && chown obscurouser:obscurouser /home/obscurouser/edb-connect.sh \ + && chmod u+x /home/obscurouser/edb-connect.sh' \ && docker network create --driver bridge node_network || true \ && docker run -d --name datadog-agent \ --network node_network \ diff --git a/.github/workflows/manual-publish-edb-connect.yml b/.github/workflows/manual-publish-edb-connect.yml new file mode 100644 index 0000000000..7f8da472b2 --- /dev/null +++ b/.github/workflows/manual-publish-edb-connect.yml @@ -0,0 +1,33 @@ +# Publishes the latest version of edb-connect to the Azure Container Registry +# Users will then have access to this latest version when they run the edb-connect.sh script on the node VMs. + +name: "[M] Publish EDB Connect" +run-name: "[M] Publish EDB Connect" +on: + workflow_dispatch: + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: "Set up Docker" + uses: docker/setup-buildx-action@v1 + + - name: "Login to Azure docker registry" + uses: azure/docker-login@v1 + with: + login-server: testnetobscuronet.azurecr.io + username: testnetobscuronet + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: "Login via Azure CLI" + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Build and Push Docker EDB Connect Image + run: | + DOCKER_BUILDKIT=1 docker build -t ${{ vars.DOCKER_BUILD_TAG_EDB_CONNECT }} -f ./tools/edbconnect/Dockerfile . + docker push ${{ vars.DOCKER_BUILD_TAG_EDB_CONNECT }} diff --git a/go/enclave/storage/init/edgelessdb/edgelessdb.go b/go/enclave/storage/init/edgelessdb/edgelessdb.go index 06d08e40b9..d9bcab2e7f 100644 --- a/go/enclave/storage/init/edgelessdb/edgelessdb.go +++ b/go/enclave/storage/init/edgelessdb/edgelessdb.go @@ -41,15 +41,15 @@ import ( ) /* - The Obscuro Enclave (OE) needs a way to persist data into a trusted database. Trusted not to reveal that data to anyone but that particular enclave. + The Ten Enclave (TE) needs a way to persist data into a trusted database. Trusted not to reveal that data to anyone but that particular enclave. - To achieve this, the OE must first perform Remote Attestation (RA), which gives it confidence that it is connected to + To achieve this, the TE must first perform Remote Attestation (RA), which gives it confidence that it is connected to a trusted version of software running on trusted hardware. The result of this process is a Certificate which can be used to set up a trusted TLS connection into the database. - The next step is to configure the database schema and users in such a way that the OE knows that the db engine will + The next step is to configure the database schema and users in such a way that the TE knows that the db engine will only allow itself access to it. This is achieved by creating a "Manifest" file that contains the SQL init code and a - DBClient Certificate that is known only to the OE. + DBClient Certificate that is known only to the TE. This "DBClient" Cert is used by the database to authenticate that it is communicating to the entity that has initialised that schema. @@ -130,6 +130,7 @@ type Credentials struct { UserKeyPEM string // db user private key, generated in our enclave } +// Connector (re-)establishes a connection to the Edgeless DB for the Ten enclave func Connector(edbCfg *Config, config config.EnclaveConfig, logger gethlog.Logger) (enclavedb.EnclaveDB, error) { // rather than fail immediately if EdgelessDB is not available yet we wait up for `edgelessDBStartTimeout` for it to be available err := waitForEdgelessDBToStart(edbCfg.Host, logger) @@ -143,12 +144,12 @@ func Connector(edbCfg *Config, config config.EnclaveConfig, logger gethlog.Logge return nil, err } - tlsCfg, err := createTLSCfg(edbCredentials) + tlsCfg, err := CreateTLSCfg(edbCredentials) if err != nil { return nil, err } - sqlDB, err := connectToEdgelessDB(edbCfg.Host, tlsCfg, logger) + sqlDB, err := ConnectToEdgelessDB(edbCfg.Host, tlsCfg, logger) if err != nil { return nil, err } @@ -183,7 +184,7 @@ func waitForEdgelessDBToStart(edbHost string, logger gethlog.Logger) error { func getHandshakeCredentials(enclaveConfig config.EnclaveConfig, edbCfg *Config, logger gethlog.Logger) (*Credentials, error) { // if we have previously performed the handshake we can retrieve the creds from disk and proceed - edbCreds, found, err := loadCredentialsFromFile() + edbCreds, found, err := LoadCredentialsFromFile() if err != nil { return nil, err } @@ -198,8 +199,8 @@ func getHandshakeCredentials(enclaveConfig config.EnclaveConfig, edbCfg *Config, return edbCreds, nil } -// loadCredentialsFromFile returns (credentials object, found flag, error), if file not found it will return nil error but found=false -func loadCredentialsFromFile() (*Credentials, bool, error) { +// LoadCredentialsFromFile returns (credentials object, found flag, error), if file not found it will return nil error but found=false +func LoadCredentialsFromFile() (*Credentials, bool, error) { b, err := egoutils.ReadAndUnseal(edbCredentialsFilepath) if err != nil { if os.IsNotExist(err) { @@ -227,8 +228,8 @@ func performHandshake(enclaveConfig config.EnclaveConfig, edbCfg *Config, logger // the RA will ensure that we are connecting to a database that will not leak any data. // The RA will return a Certificate which we'll use for the TLS mutual authentication when we connect to the database. // The trust path is as follows: - // 1. The Obscuro Enclave performs RA on the database enclave, and the RA object contains a certificate which only the database enclave controls. - // 2. Connecting to the database via mutually authenticated TLS using the above certificate, will give the Obscuro enclave confidence that it is only giving data away to some code and hardware it trusts. + // 1. The Ten Enclave performs RA on the database enclave, and the RA object contains a certificate which only the database enclave controls. + // 2. Connecting to the database via mutually authenticated TLS using the above certificate, will give the Ten enclave confidence that it is only giving data away to some code and hardware it trusts. edbPEM, err := performEDBRemoteAttestation(enclaveConfig, edbCfg.Host, defaultEDBConstraints, logger) if err != nil { return nil, err @@ -304,7 +305,7 @@ func createManifestFormat(content string) (result []string) { return } -func createTLSCfg(creds *Credentials) (*tls.Config, error) { +func CreateTLSCfg(creds *Credentials) (*tls.Config, error) { caCertPool := x509.NewCertPool() if ok := caCertPool.AppendCertsFromPEM([]byte(creds.EDBCACertPEM)); !ok { @@ -458,7 +459,7 @@ func verifyEdgelessDB(edbHost string, m *manifest, httpClient *http.Client, logg return nil } -func connectToEdgelessDB(edbHost string, tlsCfg *tls.Config, logger gethlog.Logger) (*sql.DB, error) { +func ConnectToEdgelessDB(edbHost string, tlsCfg *tls.Config, logger gethlog.Logger) (*sql.DB, error) { err := mysql.RegisterTLSConfig("custom", tlsCfg) if err != nil { return nil, fmt.Errorf("failed to register tls config for mysql connection - %w", err) diff --git a/integration/common/testlog/testlog.go b/integration/common/testlog/testlog.go index 67493fa90d..9556fd9ca4 100644 --- a/integration/common/testlog/testlog.go +++ b/integration/common/testlog/testlog.go @@ -56,3 +56,12 @@ func Setup(cfg *Cfg) *os.File { testlog = gethlog.New(log.CmpKey, log.TestLogCmp) return f } + +// SetupSysOut will direct logs to stdout +func SetupSysOut() { + err := debug.Setup("terminal", "", false, 10000000, 0, 0, false, false, slog.LevelDebug, "") + if err != nil { + panic(err) + } + testlog = gethlog.New(log.CmpKey, log.TestLogCmp) +} diff --git a/integration/networktest/tests/helpful/smoke_test.go b/integration/networktest/tests/helpful/smoke_test.go index a97bbf54a5..d5dd8da86b 100644 --- a/integration/networktest/tests/helpful/smoke_test.go +++ b/integration/networktest/tests/helpful/smoke_test.go @@ -18,7 +18,7 @@ func TestExecuteNativeFundsTransfer(t *testing.T) { networktest.Run( "native-funds-smoketest", t, - env.LocalDevNetwork(), + env.SepoliaTestnet(), actions.Series( &actions.CreateTestUser{UserID: 0}, &actions.CreateTestUser{UserID: 1}, diff --git a/tools/edbconnect/Dockerfile b/tools/edbconnect/Dockerfile new file mode 100644 index 0000000000..d8428035c0 --- /dev/null +++ b/tools/edbconnect/Dockerfile @@ -0,0 +1,41 @@ +# Build Stages: +# build-base = downloads modules and prepares the directory for compilation. Based on the ego-dev image +# build-enclave = copies over the actual source code of the project and builds it using a compiler cache +# deploy = copies over only the enclave executable without the source +# in a lightweight base image specialized for deployment and prepares the /data/ folder. + +FROM ghcr.io/edgelesssys/ego-dev:v1.5.0 AS build-base + +# setup container data structure +RUN mkdir -p /home/ten/go-ten + +# Ensures container layer caching when dependencies are not changed +WORKDIR /home/ten/go-ten +COPY go.mod . +COPY go.sum . +RUN ego-go mod download + +# Trigger new build stage for compiling the enclave +FROM build-base as build-enclave +COPY . . + +WORKDIR /home/ten/go-ten/tools/edbconnect/main + +# Build the enclave using the cross image build cache. +RUN --mount=type=cache,target=/root/.cache/go-build \ + ego-go build + +# New build stage for compiling the enclave with restricted flags mode +FROM build-enclave as sign-built-enclave +# Sign the enclave executable +RUN ego sign edb-enclave.json + + +# Trigger a new build stage and use the smaller ego version: +FROM ghcr.io/edgelesssys/ego-deploy:v1.5.0 + +# Copy the binary and the entrypoint script +COPY --from=sign-built-enclave \ + /home/ten/go-ten/tools/edbconnect/main /home/ten/go-ten/tools/edbconnect/main + +WORKDIR /home/ten/go-ten/tools/edbconnect/main diff --git a/tools/edbconnect/edb-connect.sh b/tools/edbconnect/edb-connect.sh new file mode 100644 index 0000000000..48518051d3 --- /dev/null +++ b/tools/edbconnect/edb-connect.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# Variables +IMAGE_NAME="testnetobscuronet.azurecr.io/obscuronet/edbconnect:latest" +CONTAINER_BASE_NAME="edb-connect" +UNIQUE_ID=$(date +%s%3N) # Using milliseconds for uniqueness +CONTAINER_NAME="${CONTAINER_BASE_NAME}-${UNIQUE_ID}" +VOLUME_NAME="obscuronode-enclave-volume" +NETWORK_NAME="node_network" +SGX_ENCLAVE_DEVICE="/dev/sgx_enclave" +SGX_PROVISION_DEVICE="/dev/sgx_provision" +COMMAND="ego run /home/ten/go-ten/tools/edbconnect/main/main" + +# Function to destroy exited containers matching the base name +destroy_exited_containers() { + exited_containers=$(docker ps -a -q -f name=${CONTAINER_BASE_NAME} -f status=exited) + if [ "$exited_containers" ];then + echo "Removing exited containers matching ${CONTAINER_BASE_NAME}..." + docker rm $exited_containers || true + else + echo "No exited containers to remove." + fi +} + +# Destroy exited containers that match the base name +destroy_exited_containers + +# Pull the latest image from Azure Docker repository +echo "Pulling the latest Docker image..." +docker pull $IMAGE_NAME + +# Run the container with the specified command +echo "Running the new container with name ${CONTAINER_NAME}..." +docker run --name $CONTAINER_NAME \ + --network $NETWORK_NAME \ + -v $VOLUME_NAME:/enclavedata \ + --device $SGX_ENCLAVE_DEVICE:$SGX_ENCLAVE_DEVICE:rwm \ + --device $SGX_PROVISION_DEVICE:$SGX_PROVISION_DEVICE:rwm \ + $IMAGE_NAME $COMMAND + +# After the REPL exits, destroy the container +echo "Destroying the container ${CONTAINER_NAME} after command exits..." +docker rm $CONTAINER_NAME || true \ No newline at end of file diff --git a/tools/edbconnect/main/edb-enclave.json b/tools/edbconnect/main/edb-enclave.json new file mode 100644 index 0000000000..b275f67f24 --- /dev/null +++ b/tools/edbconnect/main/edb-enclave.json @@ -0,0 +1,19 @@ +{ + "exe": "main", + "key": "testnet.pem", + "debug": true, + "heapSize": 4096, + "executableHeap": true, + "productID": 1, + "securityVersion": 1, + "mounts": [ + { + "source": "/enclavedata", + "target": "/data", + "type": "hostfs", + "readOnly": false + } + ], + "env": [ + ] +} \ No newline at end of file diff --git a/tools/edbconnect/main/main.go b/tools/edbconnect/main/main.go new file mode 100644 index 0000000000..20ed19873a --- /dev/null +++ b/tools/edbconnect/main/main.go @@ -0,0 +1,185 @@ +package main + +import ( + "bufio" + "database/sql" + "fmt" + "os" + "strings" + + "github.com/ten-protocol/go-ten/go/enclave/storage/init/edgelessdb" + "github.com/ten-protocol/go-ten/integration/common/testlog" +) + +func main() { + fmt.Println("Retrieving Edgeless DB credentials...") + creds, found, err := edgelessdb.LoadCredentialsFromFile() + if err != nil { + fmt.Println("Error loading credentials from file:", err) + panic(err) + } + if !found { + panic("No existing EDB credentials found.") + } + fmt.Println("Found existing EDB credentials. Creating TLS config...") + cfg, err := edgelessdb.CreateTLSCfg(creds) + if err != nil { + fmt.Println("Error creating TLS config from credentials:", err) + panic(err) + } + fmt.Println("TLS config created. Connecting to Edgeless DB...") + testlog.SetupSysOut() + db, err := edgelessdb.ConnectToEdgelessDB("obscuronode-edgelessdb", cfg, testlog.Logger()) + if err != nil { + fmt.Println("Error connecting to Edgeless DB:", err) + panic(err) + } + fmt.Println("Connected to Edgeless DB.") + + startREPL(db) + + err = db.Close() + if err != nil { + fmt.Println("Error closing Edgeless DB connection:", err) + panic(err) + } +} + +// Starts a loop that reads user input and runs queries against the Edgeless DB until user types "exit" +func startREPL(db *sql.DB) { + reader := bufio.NewReader(os.Stdin) + for { + fmt.Println("\nEnter a query to run against the Edgeless DB (or type 'exit' to quit):") + fmt.Print(">>> ") + query, err := reader.ReadString('\n') + if err != nil { + fmt.Println("Error reading user input:", err) + return + } + + // Trim the newline character and surrounding whitespace + query = strings.TrimSpace(query) + + if query == "" { + continue + } + + if query == "exit" { + break + } + + // Determine the type of query, so we can show appropriate output + queryType := strings.ToUpper(strings.Split(query, " ")[0]) + switch queryType { + case "SELECT", "SHOW", "DESCRIBE", "DESC", "EXPLAIN": + // output rows + runQuery(db, query) + default: + // output number of rows affected + runExec(db, query) + } + } + fmt.Println("Exiting...") +} + +func runQuery(db *sql.DB, query string) { + rows, err := db.Query(query) + if err != nil { + fmt.Println("Error executing query:", err) + return + } + defer rows.Close() + + cols, err := rows.Columns() + if err != nil { + fmt.Println("Error fetching columns:", err) + return + } + + // Print column headers + for _, colName := range cols { + fmt.Printf("%s\t", colName) + } + fmt.Println() + + // Prepare a slice to hold the values + values := make([]interface{}, len(cols)) + valuePtrs := make([]interface{}, len(cols)) + for rows.Next() { + for i := range values { + valuePtrs[i] = &values[i] + } + + err = rows.Scan(valuePtrs...) + if err != nil { + fmt.Println("Error scanning row:", err) + return + } + + // Print the row values + for _, val := range values { + // Handle NULL values and convert byte slices to strings + switch v := val.(type) { + case nil: + fmt.Print("NULL\t") + case []byte: + if isPrintableString(v) { + fmt.Printf("%s\t", string(v)) + } else { + fmt.Printf("%x\t", v) // Print binary data as hexadecimal + } + default: + fmt.Printf("%v\t", v) + } + } + fmt.Println() + } + + if err = rows.Err(); err != nil { + fmt.Println("Error during row iteration:", err) + } +} + +func runExec(db *sql.DB, query string) { + result, err := db.Exec(query) + if err != nil { + fmt.Println("Error executing query against Edgeless DB:", err) + return + } + rowsAffected, err := result.RowsAffected() + if err != nil { + fmt.Println("Error getting number of rows affected:", err) + return + } + fmt.Println("Number of rows affected:", rowsAffected) +} + +// isPrintableString checks if a byte slice contains only printable characters +func isPrintableString(data []byte) bool { + for _, b := range data { + if b < 32 || b > 126 { + return false + } + } + return true +} + +//func main() { +// fmt.Println("Checking for existing EDB credentials...") +// file, found, err := edgelessdb.LoadCredentialsFromFile() +// if err != nil { +// fmt.Println("Error loading credentials from file:", err) +// panic(err) +// } +// if !found { +// panic("No existing EDB credentials found.") +// } +// +// credJson, err := json.Marshal(file) +// if err != nil { +// fmt.Println("Error loading credentials from file:", err) +// panic(err) +// } +// fmt.Println("Found existing EDB credentials in file:", credJson) +// +//} diff --git a/tools/edbconnect/main/testnet.pem b/tools/edbconnect/main/testnet.pem new file mode 100644 index 0000000000..832ae56944 --- /dev/null +++ b/tools/edbconnect/main/testnet.pem @@ -0,0 +1,39 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIG4QIBAAKCAYEAmJTM/Ik4i3JzKGvNc8gNWBKlh77oKte2raJFpsTDTLEFN105 +dPgyb+29kaxh200IgzE4PyBaCMCyG8KyuIbwNlfPSF+bDsy1L+U43IkKpr1JdzOb +O/RtP1X8iwTSu8wPinP1hEPraJvv0LSDbchW+QGNnXclrPwgnQm31erOCO1qAmxV +YIR55h+5xREOOut9MbovaSveUGrMcoxuy2t079a9nDbPsYRnt1lrXSZ8wBOLwJxZ +QkD3AK1667PozDcab7JD+grzg4FhwepJ7F3SsIlK8VaN+4C7eZRvEnxXOEteNu7w +I5dqziN3d7qdVwPGiRQFmVF89u96B3YcVxJKQDVsIktatHwkvTWYhIZllf0d/P01 +8voV16JTmzpJpClBnuOA8YNUSaxzkWTYQWy+nLcUEXAF75JjavvVC1rHRtybp8pL +ZgBrh85qOUQErgFhYX4aK9w48XfCcI8RLLBDFZNIXWX4+p9rAdb6Itei/qVdBA05 +KQ2X5nmiX7gu3xwTAgEDAoIBgGW4iKhbewehohryiPfas5AMblp/RXHlJHPBg8SD +LN3LWM+Te6NQIZ/z07Zy6+eIsFd2JX9q5rCAdr0sdyWvSs7lNNrqZ18zI3VDez2w +scR+MPoiZ31Nnio5UwdYjH0ytQb3+QLX8kW9SosjAkkwOftWXmj6GR39axNbz+Px +3rCeRqxIOOsC++6/0S4LXtHyU3Z8H5tylDWciExdnzJHo0qPKRLPNSEC78+Q8j4Z +qIANB9W9kNbV+gBzp0fNRd16EJhHsR04XClI14TDUX3x+uLsDtowJMgy7bE43k17 +MJ8oghThz8iQhGxEsQO7QXMrknUQ3BkHpiT/kKgw403IDoGCsugyTh/xkecVZPUF +m1TZ4TqzHRlJMdn2G2jxuSPzEAf5dJGIXkzOp0Hzy+/tEI8VTnuzg6RtRWCpxt52 +RJkHItOtSCG+cXfe8ALEv04eIs2/y/8xHwp8gYrSlWOGA1ILL6Gj5oGxBFcA03xr +n6dFL0GSSLWbomq4DNwANJrCSwKBwQDF0l5DROmUqt3rQAtXn4VhyMN9cpjC9yeJ +KxelRkR8bR5K70ooAFKADVVJbc2g000Tv8ldUNcDaECVw2V2H11T7e8mR2+HwZRL +JHt33vyUciS5z6ZB3vSlcgop/3o+TcAxEntBFGKRuircfI/ItW8Dr6e2GJsHVdx0 +1ZohVyEHAAdGgmwOPAVSgWWX3hIC2XwHbDDFmZSz//GIjeIO7lknScfT0bC6iqky +rqrEWcJfbWruYlqIMDHLycKjDRZKq0sCgcEAxXRcCvF/sOlAj4VEV9NU+l5xLJu+ +DD0vZpQJ+P1JzSF8zKzuTr5Rq68YqLPtiW8dxbryFnUsvAfgdWlh7EbXKgNwn7h1 +/NA1l3EFnR8AAkQnayDkCy1Waz8gU9A5r+7pYdrW1iJkRLxN0fqWkNO2wmd2ocol +cZie5SeQnFI/WlHgI8PzJSa8AX6cnT7TtfqxJXI3Z3j1rb0Ol8VPCHjk8zi5Fx5u +fYs7TKcSI9xxJFArM09xkHPyepvMcqrJrE1ZAoHBAIPhlCzYm7hx6UeAB4+/rkEw +glOhuyykxQYcum4u2FLzaYdKMXAANwAI44ZJM8CM3g0qhj415KzwKw6CQ6QU6OKe +n27aSlqBDYdtp6U/Uw2hbdE1GYE/TcOhXBv/ptQz1XYMUiti7GEmxz2oX9sjn1fK +b867EgTj6E3jvBY6FgSqr4RW8rQoA4xWQ7qUDAHmUq+dddkRDc1VS7BelrSe5hox +L+KLyycHG3cfHILmgZTznJ7sPFrKy90xLGyzZDHHhwKBwQCDougHS6p18NW1A4Ll +N438PvYdvSldfh+ZuAal/jEza6iIc0mJ1DZydLsbIp5bn2kufKFkTh3Sr+r48Ovy +2eTGrPW/0E6ois5k9gO+FKqsLW+cwJgHc47yKhWNNXvKn0ZBPI85bELYfYk2pw8L +N88sRPnBMW5LuxSYxQsS4X+Ri+rCgqIYxH1WVGho1I0j/HYY9s+aUKPJKLRlLjSw +UJiiJdC6FEmpB3zdxLbCkvYYNXIiNPZgTUxRvTL3HIZy3jsCgb8ZLq3nA5jZtEli +g86vAnCLGjHyscz0oBqlpxPyAJF1y6ldm/ySdiGav4mai3BqNKQnUKwbxnPlMYbO +NR52ofDfCaiLElJChr+E0VPyu/cBLBmLYpZKwSo5XkRQENHWpqQoxhPM3a866sL/ +PSo2mdwX67OgJHGr+Gyfq6K6rNJeOGNkm1C/y8tw932GFnnMt6IxgWsJOxrV6o8R ++/k0iY10nu2ZvaDrmM6irlTKxNTbgUXqR/CgxIuMbcfZpo4UxQ== +-----END RSA PRIVATE KEY-----