Skip to content

Add support for mTLS authentication in Arrow Flight client #25179

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ public enum ArrowErrorCode
ARROW_INTERNAL_ERROR(1, INTERNAL_ERROR),
ARROW_FLIGHT_CLIENT_ERROR(2, EXTERNAL),
ARROW_FLIGHT_METADATA_ERROR(3, EXTERNAL),
ARROW_FLIGHT_TYPE_ERROR(4, EXTERNAL);
ARROW_FLIGHT_TYPE_ERROR(4, EXTERNAL),
ARROW_FLIGHT_INVALID_KEY_ERROR(5, INTERNAL_ERROR),
ARROW_FLIGHT_INVALID_CERT_ERROR(6, INTERNAL_ERROR);

private final ErrorCode errorCode;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@
package com.facebook.plugin.arrow;

import com.facebook.airlift.configuration.Config;
import com.facebook.airlift.configuration.ConfigDescription;

public class ArrowFlightConfig
{
private String server;
private boolean verifyServer = true;
private String flightServerSSLCertificate;
private String flightClientSSLCertificate;
private String flightClientSSLKey;
private boolean arrowFlightServerSslEnabled;
private Integer arrowFlightPort;

Expand Down Expand Up @@ -82,4 +85,30 @@ public ArrowFlightConfig setArrowFlightServerSslEnabled(boolean arrowFlightServe
this.arrowFlightServerSslEnabled = arrowFlightServerSslEnabled;
return this;
}

public String getFlightClientSSLCertificate()
{
return flightClientSSLCertificate;
}

@ConfigDescription("Path to the client SSL certificate used for mTLS authentication with Flight server")
@Config("arrow-flight.client-ssl-certificate")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be good to add a comment here saying this is needed for mTLS auth.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comments.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of a javadoc, could you put this in a @ConfigDescription?

public ArrowFlightConfig setFlightClientSSLCertificate(String flightClientSSLCertificate)
{
this.flightClientSSLCertificate = flightClientSSLCertificate;
return this;
}

public String getFlightClientSSLKey()
{
return flightClientSSLKey;
}

@ConfigDescription("Path to the client SSL key used for mTLS authentication with Flight server")
@Config("arrow-flight.client-ssl-key")
public ArrowFlightConfig setFlightClientSSLKey(String flightClientSSLKey)
{
this.flightClientSSLKey = flightClientSSLKey;
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/
package com.facebook.plugin.arrow;

import com.facebook.airlift.log.Logger;
import com.facebook.presto.spi.ConnectorSession;
import com.facebook.presto.spi.SchemaTableName;
import org.apache.arrow.flight.CallOption;
Expand All @@ -30,11 +31,15 @@
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.file.Paths;
import java.security.InvalidKeyException;
import java.security.cert.CertificateException;
import java.util.List;
import java.util.Optional;

import static com.facebook.plugin.arrow.ArrowErrorCode.ARROW_FLIGHT_CLIENT_ERROR;
import static com.facebook.plugin.arrow.ArrowErrorCode.ARROW_FLIGHT_INFO_ERROR;
import static com.facebook.plugin.arrow.ArrowErrorCode.ARROW_FLIGHT_INVALID_CERT_ERROR;
import static com.facebook.plugin.arrow.ArrowErrorCode.ARROW_FLIGHT_INVALID_KEY_ERROR;
import static com.facebook.plugin.arrow.ArrowErrorCode.ARROW_FLIGHT_METADATA_ERROR;
import static java.nio.file.Files.newInputStream;
import static java.util.Objects.requireNonNull;
Expand All @@ -43,6 +48,7 @@ public abstract class BaseArrowFlightClientHandler
{
private final ArrowFlightConfig config;
private final BufferAllocator allocator;
private static final Logger logger = Logger.get(BaseArrowFlightClientHandler.class);

public BaseArrowFlightClientHandler(BufferAllocator allocator, ArrowFlightConfig config)
{
Expand All @@ -64,24 +70,60 @@ protected FlightClient createFlightClient()

protected FlightClient createFlightClient(Location location)
{
Optional<InputStream> trustedCertificate = Optional.empty();
Optional<InputStream> clientCertificate = Optional.empty();
Optional<InputStream> clientKey = Optional.empty();
try {
Optional<InputStream> trustedCertificate = Optional.empty();
FlightClient.Builder flightClientBuilder = FlightClient.builder(allocator, location);
flightClientBuilder.verifyServer(config.getVerifyServer());
if (config.getFlightServerSSLCertificate() != null) {
trustedCertificate = Optional.of(newInputStream(Paths.get(config.getFlightServerSSLCertificate())));
flightClientBuilder.trustedCertificates(trustedCertificate.get()).useTls();
}

FlightClient flightClient = flightClientBuilder.build();
if (trustedCertificate.isPresent()) {
trustedCertificate.get().close();
if (config.getFlightClientSSLCertificate() != null && config.getFlightClientSSLKey() != null) {
clientCertificate = Optional.of(newInputStream(Paths.get(config.getFlightClientSSLCertificate())));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trying to understand it a bit better, what happens if the certificate and key are invalid? Lets use a try-catch block here? And maybe add a test case covering this scenario?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the cert is invalid, executing a query will give the user an error that the cert is invalid. Added a test case that covers this scenario.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets add a try-catch here as well and modify the error message in the test case accordingly. Thank you for adding test case for this though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The invalid cert exception is thrown only at line 84 FlightClient flightClient = flightClientBuilder.build(); and we might get exception due to other reasons as well from the build method. So adding a try...catch here will not help in modifying the error message.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I actually foresee improperly configured client cert/key as a very probable source of error, and hence wanted to cover the scenario with a proper user facing message. Anyways I leave the final decision to you.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored the code to catch errors due to invalid cert or key file. Rethrowing a Presto Exception with a custom message for those scenarios.

clientKey = Optional.of(newInputStream(Paths.get(config.getFlightClientSSLKey())));
flightClientBuilder.clientCertificate(clientCertificate.get(), clientKey.get()).useTls();
}

return flightClient;
return flightClientBuilder.build();
}
catch (Exception e) {
throw new ArrowException(ARROW_FLIGHT_CLIENT_ERROR, "Error creating flight client: " + e.getMessage(), e);
if (e.getCause() instanceof InvalidKeyException) {
throw new ArrowException(ARROW_FLIGHT_INVALID_KEY_ERROR, "Error creating flight client, invalid key file: " + e.getMessage(), e);
}
else if (e.getCause() instanceof CertificateException) {
throw new ArrowException(ARROW_FLIGHT_INVALID_CERT_ERROR, "Error creating flight client, invalid certificate file: " + e.getMessage(), e);
}
else {
throw new ArrowException(ARROW_FLIGHT_CLIENT_ERROR, "Error creating flight client: " + e.getMessage(), e);
}
}
finally {
if (trustedCertificate.isPresent()) {
try {
trustedCertificate.get().close();
}
catch (IOException e) {
logger.error("Error closing input stream for server certificate", e);
}
}
if (clientCertificate.isPresent()) {
try {
clientCertificate.get().close();
}
catch (IOException e) {
logger.error("Error closing input stream for client certificate", e);
}
}
if (clientKey.isPresent()) {
try {
clientKey.get().close();
}
catch (IOException e) {
logger.error("Error closing input stream for client key", e);
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.plugin.arrow;

import com.facebook.airlift.log.Logger;
import com.facebook.plugin.arrow.testingServer.TestingArrowProducer;
import com.facebook.presto.testing.QueryRunner;
import com.facebook.presto.tests.AbstractTestQueryFramework;
import com.facebook.presto.tests.DistributedQueryRunner;
import com.google.common.collect.ImmutableMap;
import org.apache.arrow.flight.FlightServer;
import org.apache.arrow.flight.Location;
import org.apache.arrow.memory.RootAllocator;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;

import static com.facebook.plugin.arrow.testingConnector.TestingArrowFlightPlugin.ARROW_FLIGHT_CONNECTOR;

public class TestArrowFlightMtls
extends AbstractTestQueryFramework
{
private static final Logger logger = Logger.get(TestArrowFlightMtls.class);
private final int serverPort;
private RootAllocator allocator;
private FlightServer server;
private DistributedQueryRunner arrowFlightQueryRunner;
private static final String ARROW_FLIGHT_CATALOG_WITH_INVALID_CERT = "arrow_catalog_with_invalid_cert";
private static final String ARROW_FLIGHT_CATALOG_WITH_NO_MTLS_CERTS = "arrow_catalog_with_no_mtls_certs";
private static final String ARROW_FLIGHT_CATALOG_WITH_MTLS_CERTS = "arrow_catalog_with_mtls_certs";

public TestArrowFlightMtls()
throws IOException
{
this.serverPort = ArrowFlightQueryRunner.findUnusedPort();
}

@BeforeClass
private void setup()
throws Exception
{
arrowFlightQueryRunner = getDistributedQueryRunner();
arrowFlightQueryRunner.createCatalog(ARROW_FLIGHT_CATALOG_WITH_INVALID_CERT, ARROW_FLIGHT_CONNECTOR, getInvalidCertCatalogProperties());
arrowFlightQueryRunner.createCatalog(ARROW_FLIGHT_CATALOG_WITH_NO_MTLS_CERTS, ARROW_FLIGHT_CONNECTOR, getNoMtlsCatalogProperties());
arrowFlightQueryRunner.createCatalog(ARROW_FLIGHT_CATALOG_WITH_MTLS_CERTS, ARROW_FLIGHT_CONNECTOR, getMtlsCatalogProperties());

File certChainFile = new File("src/test/resources/mtls/server.crt");
File privateKeyFile = new File("src/test/resources/mtls/server.key");
File caCertFile = new File("src/test/resources/mtls/ca.crt");

allocator = new RootAllocator(Long.MAX_VALUE);

Location location = Location.forGrpcTls("localhost", serverPort);
server = FlightServer.builder(allocator, location, new TestingArrowProducer(allocator))
.useTls(certChainFile, privateKeyFile)
.useMTlsClientVerification(caCertFile)
.build();

server.start();
logger.info("Server listening on port %s", server.getPort());
}

@AfterClass(alwaysRun = true)
private void tearDown()
throws InterruptedException
{
arrowFlightQueryRunner.close();
server.close();
allocator.close();
}

@Override
protected QueryRunner createQueryRunner()
throws Exception
{
return ArrowFlightQueryRunner.createQueryRunner(serverPort, ImmutableMap.of(), ImmutableMap.of(), Optional.empty());
}

private Map<String, String> getInvalidCertCatalogProperties()
{
ImmutableMap.Builder<String, String> catalogProperties = ImmutableMap.<String, String>builder()
.put("arrow-flight.server.port", String.valueOf(serverPort))
.put("arrow-flight.server", "localhost")
.put("arrow-flight.server-ssl-enabled", "true")
.put("arrow-flight.server-ssl-certificate", "src/test/resources/mtls/server.crt")
.put("arrow-flight.client-ssl-certificate", "src/test/resources/mtls/invalid_cert.crt")
.put("arrow-flight.client-ssl-key", "src/test/resources/mtls/client.key");
return catalogProperties.build();
}

private Map<String, String> getNoMtlsCatalogProperties()
{
ImmutableMap.Builder<String, String> catalogProperties = ImmutableMap.<String, String>builder()
.put("arrow-flight.server.port", String.valueOf(serverPort))
.put("arrow-flight.server", "localhost")
.put("arrow-flight.server-ssl-enabled", "true")
.put("arrow-flight.server-ssl-certificate", "src/test/resources/mtls/server.crt");
return catalogProperties.build();
}

private Map<String, String> getMtlsCatalogProperties()
{
ImmutableMap.Builder<String, String> catalogProperties = ImmutableMap.<String, String>builder()
.put("arrow-flight.server.port", String.valueOf(serverPort))
.put("arrow-flight.server", "localhost")
.put("arrow-flight.server-ssl-enabled", "true")
.put("arrow-flight.server-ssl-certificate", "src/test/resources/mtls/server.crt")
.put("arrow-flight.client-ssl-certificate", "src/test/resources/mtls/client.crt")
.put("arrow-flight.client-ssl-key", "src/test/resources/mtls/client.key");
return catalogProperties.build();
}

@Test
public void testMtlsInvalidCert()
{
assertQueryFails("SELECT COUNT(*) FROM " + ARROW_FLIGHT_CATALOG_WITH_INVALID_CERT + ".tpch.orders", ".*invalid certificate file.*");
}

@Test
public void testMtlsFailure()
{
assertQueryFails("SELECT COUNT(*) FROM " + ARROW_FLIGHT_CATALOG_WITH_NO_MTLS_CERTS + ".tpch.orders", "ssl exception");
}

@Test
public void testMtls()
{
assertQuery("SELECT COUNT(*) FROM " + ARROW_FLIGHT_CATALOG_WITH_MTLS_CERTS + ".tpch.orders", "SELECT COUNT(*) FROM orders");
}
}
33 changes: 33 additions & 0 deletions presto-base-arrow-flight/src/test/resources/mtls/ca.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
-----BEGIN CERTIFICATE-----
MIIFozCCA4ugAwIBAgIUTnZhzGzxAzaTJ5DDQnmBp8jvBUMwDQYJKoZIhvcNAQEL
BQAwYDELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5
MQ4wDAYDVQQKDAVNeU9yZzEPMA0GA1UECwwGTXlVbml0MREwDwYDVQQDDAhNeVJv
b3RDQTAgFw0yNTA1MjIxNjA2MDNaGA8yMTI1MDQyODE2MDYwM1owYDELMAkGA1UE
BhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MQ4wDAYDVQQKDAVN
eU9yZzEPMA0GA1UECwwGTXlVbml0MREwDwYDVQQDDAhNeVJvb3RDQTCCAiIwDQYJ
KoZIhvcNAQEBBQADggIPADCCAgoCggIBALmNRxPZt9vvFvIQcdCM3xcAOiTepahR
zsK86AwzXbwcWwOw+rQP0iXiMGjP9wJy0C1hU+j7F+gu5+d5j3DK9/iTlIqByjN+
gffsZDSBBZBozJxARBWKaEFKRKbhCmr+v92ZN1KMYhhwc5W3SRp6y3kvsWYM4/JE
pRzYUy0505g3p7PnpQzZZSSzV33+9fn5ggafVB9FSJqOuBKUBTITt+Rf1pNV2ZGC
f2X39KOIIRz2Hz1L6hdqeMQN9V5KQ7C6oPfl+ho97JHaZirtHr1XZJ6YQRy7Mldp
jOPj4MMKqi8m+HhyCSznIDNiMp0OACX3CZ5RmKRYfnunOuw+fvI28HyOXMgfWKa2
o/2/YlAzNrD50hJPwQMTlKWJm2gY2n5x2FWT6/8aXeCnsJALK6Dj6Ax0wxkAFfIo
76REHlXX2fIiz0cciYwYtvwhjp5efqX22B7LDkhu7fJ42yUd6g3crbmGM8OOoXgL
w3MWx30FatTyDT8un2ZvVDJEADW3+WWtyrZWHFMVFrVnN7Di4MAuWsRIVtuO6PEV
6pPS55NBmvKzWAoYBmpH103GlIxvZ6CCTeKqFbcrI77smrIO5CLmzl5yjb/urmy4
GLYHM+EPB5pJKQO8g6fyM369mhEjBxt/RJGv9E0Jw7KmnHn5V2qSn4Yi41dUp7Cp
pQVIaYlJGn65AgMBAAGjUzBRMB0GA1UdDgQWBBRjt9KoJO8CO/Cy/xTJztrbdUlv
9DAfBgNVHSMEGDAWgBRjt9KoJO8CO/Cy/xTJztrbdUlv9DAPBgNVHRMBAf8EBTAD
AQH/MA0GCSqGSIb3DQEBCwUAA4ICAQApizBpSaZDYjBhOxD6+B5em18OIf5ZIjHk
iAhuppR+HaqyAHJgCO707dZVmFz9ECUZI9mvKcmj+h0Wh/mK4cSiDunFB9yUr67U
wV5F2/u/JAAq6VsbdrDiZPUwET8U9ai14LMEgPPF+Zif+wnopRav5lbiPoJVUjqr
wVoP2AHijIP46YCWwXqOTJMC79ccUMBZeDwF4bOquIADLmEnANp6fiMI+eE6OLFs
fDtjFqybRUZqzewv2lpzH2ZYEYk4bIk76TGkOYtrwJ+iQj77ZZFSBW5zkry/zaG3
/5Ufjv65T9Zr1jmIMigcmCHwNsCLOYzIKjRaiuLGs4B9s8SGEauTRhP0dG5ndWPw
50NeSNJr37MHdKky44WAFlAk9BAKlghOaC5m2RyMof8DwYKPEe5epe1wBotiPqSX
doaZvch6wkuo8xvFKqH6rBTWJLMwuFt7m3XrGqGYlE+1gvuEfn7ZGzG00sl218mZ
MfsLqJfft92ARC1/qJvUFr5mM6SV4eQeTl1tAtv6Xfczr+3/iqc5gbeG25dXQclO
y1qIKthAoXFq6rAZ+bvfASiVV1OQS76nWSiYYS1dDPQJ/g4aawOkUYr9OjS5HNQ+
rNAcLB+I0oaZQzZ85098qAVAJ76eFATb4ieDK6m0j6Fq5ddwrlYzxEEr7TscfHW2
zPCtinUe0w==
-----END CERTIFICATE-----
27 changes: 27 additions & 0 deletions presto-base-arrow-flight/src/test/resources/mtls/client.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN CERTIFICATE-----
MIIEkDCCAnigAwIBAgIUUIByH9V7DhUf5n3Qd7tPxpPixQYwDQYJKoZIhvcNAQEL
BQAwYDELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5
MQ4wDAYDVQQKDAVNeU9yZzEPMA0GA1UECwwGTXlVbml0MREwDwYDVQQDDAhNeVJv
b3RDQTAgFw0yNTA1MjIxNjA4MzFaGA8yMTI1MDQyODE2MDgzMVowXjELMAkGA1UE
BhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MQ4wDAYDVQQKDAVN
eU9yZzEPMA0GA1UECwwGTXlVbml0MQ8wDQYDVQQDDAZjbGllbnQwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8DK/RdBF6I+k+DMGrjhMMBCnpPNwJtzJU
uXYcHFYEdBnHY/rpjk/fi+7jD8bppynCZPakrDX+5VIMzS4HBU/CHY26eR2ItiWq
DoDkPAlCdgeKIGNYYEvVSuUW5YQX6fuD8PfCpCP5zK7DJC2xTTsyEjBzD+MnIB7T
ja9/22Djo2Ib2l9BEBOD+k79caPFtSqDQVdS5JLJ/P7BeqGuFS8bEgtLwwCzRxPK
kG64rXb9F0IErGwjXi/70BA24EW0uGAzzeY5Pnlx5MulEIjuxII/dTuoo+/uirN0
wxURKSzTMyzzoJqGX9Ng9+Z+VqWFrnqckcmFcD4T0sPoHpWZsYIHAgMBAAGjQjBA
MB0GA1UdDgQWBBRW/6j5rNwnwTBAXY5igXm3ETzv/DAfBgNVHSMEGDAWgBRjt9Ko
JO8CO/Cy/xTJztrbdUlv9DANBgkqhkiG9w0BAQsFAAOCAgEAVLpfZDgkL1Dz/+Fq
vSl2IoxOFNd2DTa6yM8/1wpvMVTA024lp0ttyoz0o1621hTRexcXTimZqUNAtPV6
Gwmb2ACLN4XtLk9QT9XjDtWKzPxCJ+ze7rrhj1jYqv9yUebdkJoMKfcbwYi0gtpt
HlaJqNoKgzZxOCGhTtdS3ypb9nDCyx3fmFk5mIYfzEszoMmqNL006ANlJ0IKkFZj
vUkkFyMGLerInmTDRjDLkUCkNKaJUYjZhf/FNwVtc1A9a/bDJMEYVog+CY0dpXKb
1IGaXzB4ewhuQKuhb/LCZT1pNm/cGCY2cRGFy9EVAuZ5FV0ajh1HYwdpGDzucoUD
UaTHIK40E2/kZorJa37Xyn7Lekgun6YpfOudBkKg5mlV6qoL9W/lTZPjMHs2ufvW
/A5S4okR4JmhC44TMgAv90MU9yEP90OkzW6egatBShWySJ3Bn5W+ebQSwJ38wgTy
e6j5jWh7xiiPC4TJbSXVMGEfJw/c2hx4R/83MqBhVLPfoapaUCDUWniv6n7zl5ML
k7WIZzXSK212/H+eVXFJ1Gq6zztOPkN9QGgr+dbsCzdLWPJzZDmI7+lgT2EKxIV/
VMtHVOe2bLkePTNA2+vXQhs0p7JDtzyATAyMdhJwPljt8X+HJxEoAP4Dk6PcK3Bd
E92yOuW2jl6FzgqKqtVQvxIiBgc=
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions presto-base-arrow-flight/src/test/resources/mtls/client.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8DK/RdBF6I+k+
DMGrjhMMBCnpPNwJtzJUuXYcHFYEdBnHY/rpjk/fi+7jD8bppynCZPakrDX+5VIM
zS4HBU/CHY26eR2ItiWqDoDkPAlCdgeKIGNYYEvVSuUW5YQX6fuD8PfCpCP5zK7D
JC2xTTsyEjBzD+MnIB7Tja9/22Djo2Ib2l9BEBOD+k79caPFtSqDQVdS5JLJ/P7B
eqGuFS8bEgtLwwCzRxPKkG64rXb9F0IErGwjXi/70BA24EW0uGAzzeY5Pnlx5Mul
EIjuxII/dTuoo+/uirN0wxURKSzTMyzzoJqGX9Ng9+Z+VqWFrnqckcmFcD4T0sPo
HpWZsYIHAgMBAAECggEAViw6JXFa0O3D5HtUBJmGgOsniYoqCwm4NrsGNLuHb2ME
rSpTwNNGJtqpDcQdEtVXfY1muO9xjuznPJaJkQ4ODpYcbGcz8YIGoHck+XHJjHsp
2VIeNFFsbsFzWZqzfYHrj/rMjpVJJx90tlfN2IHbroZHTXLqVPOTLL6wvZZ6P9XF
zqpWABKOaEDbenhSFFZeF5KR3NG9HSTm4YLuekumkH+QgrveDfDwXG4hAHqg836o
OF3NPaij6VlSR18nuyW0wMs/Ceu13P+GALqHmz98pFyVgHWQFryL9IccvJQDyEnt
saeG4IAVlJbZDGTnRgANLhpwBr7XhMG1aK+wmOMRgQKBgQDkcatiATlr9L8gfnHb
6pmX//AZLdXuQLXfuTvu638Brhm770noLgfIC+HIp5kCHxT2Xj5Vn+MSnYD6R6Wh
chApRKJUdsuz1iOq23YJjvsSLWCGpl9IxR7WY27uGOPIjQcOd1PRbkCq9AgUJwyn
ryca3sbYh/XQOWGLbJNIQs/S/QKBgQDSu6PVeMaS3276KblvGIvvaSAQDQWxXcC+
sA4CBmvjzx3xx5GAox/w7tcKmK/KQxNhaYy6N7xLc1YUJ9FbnT2PZQJhtP2d2Gat
Zre/+Qa+u84cR5hj9EI+B8FjW7D/psEj16KjHCds/SET6ngPM+RdB4N9daVFCurt
p0f717yiUwKBgBTJDun06I+dDkLbnmp/FwiQff0cgYmTE7lOdliPzteNSsQhypy4
i3a1Ng72yOI7h8G+43cQ/C02bYTYPgbJhRTsLMT4piIvysECBORrwQZvYIf/3U2W
ue6Rz4cUdq1Jv6meS98TZAjp+U40G1+qfSlhub/75u7SOcDg2SnLAnPVAoGBAIOO
EmRE5qpwA+b2P0Ykq89E8Hg0uPYWEiq427XV7mqkNQxoSuRkcZ9Ga0a5NRzurN2m
N+1UuB7eHMGubdtkmTa4lzkJ9T4iB09/DX0x6E0QD0bGR1M2/FefHdJ6PlAK+Q34
Ixbyj4ZRq+G0AUl0Wr7c3vBmjktA2pKMWLrW3nLzAoGBAKTl7qX6CD42gAJuT5Hp
rrXqlppVIyRvuXzXtX/Xq81IUHlBgS/t9HPyqDzmTKfxD8540kI+15bWPDHSJxiQ
ccqPaKyXhBXstDwGmlPKVzJUxk0dz5NHs+8gItUDOg78pM3siXN7vW9XBCH7mCDA
4zet/C0YCAiFVT+ipMoXy8Nc
-----END PRIVATE KEY-----
Empty file.
Loading
Loading