Skip to content
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

[improve][client] AuthenticationAthenz supports Copper Argos #19445

Merged
merged 1 commit into from
Feb 8, 2023
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 @@ -18,9 +18,13 @@
*/
package org.apache.pulsar.client.impl.auth;

import static com.google.common.base.Preconditions.checkArgument;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import com.google.common.io.CharStreams;
import com.oath.auth.KeyRefresher;
import com.oath.auth.KeyRefresherException;
import com.oath.auth.Utils;
import com.yahoo.athenz.auth.ServiceIdentityProvider;
import com.yahoo.athenz.auth.impl.SimpleServiceIdentityProvider;
import com.yahoo.athenz.auth.util.Crypto;
Expand All @@ -33,12 +37,15 @@
import java.net.URISyntaxException;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.PrivateKey;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.net.ssl.SSLContext;
import org.apache.pulsar.client.api.Authentication;
import org.apache.pulsar.client.api.AuthenticationDataProvider;
import org.apache.pulsar.client.api.EncodedAuthenticationParameterSupport;
Expand All @@ -53,13 +60,17 @@ public class AuthenticationAthenz implements Authentication, EncodedAuthenticati

private static final String APPLICATION_X_PEM_FILE = "application/x-pem-file";

private transient KeyRefresher keyRefresher = null;
private transient ZTSClient ztsClient = null;
private String ztsUrl;
private String ztsUrl = null;
private String tenantDomain;
private String tenantService;
private String providerDomain;
private PrivateKey privateKey;
private PrivateKey privateKey = null;
private String keyId = "0";
private String privateKeyPath = null;
private String x509CertChainPath = null;
private String caCertPath = null;
private String roleHeader = null;
// If auto prefetching is enabled, application will not complete until the static method
// ZTSClient.cancelPrefetch() is called.
Expand All @@ -70,7 +81,8 @@ public class AuthenticationAthenz implements Authentication, EncodedAuthenticati
// athenz will only give this token if it's at least valid for 2hrs
private static final int minValidity = 2 * 60 * 60;
private static final int maxValidity = 24 * 60 * 60; // token has upto 24 hours validity
private static final int cacheDurationInHour = 1; // we will cache role token for an hour then ask athenz lib again
private static final int cacheDurationInMinutes = 90; // role token is cached for 90 minutes
private static final int retryFrequencyInMillis = 60 * 60 * 1000; // key refresher scans files every hour

private final ReadWriteLock cachedRoleTokenLock = new ReentrantReadWriteLock();

Expand Down Expand Up @@ -116,17 +128,14 @@ private boolean cachedRoleTokenIsValid() {
if (roleToken == null) {
return false;
}
// Ensure we refresh the Athenz role token every hour to avoid using an expired
// Ensure we refresh the Athenz role token every 90 minutes to avoid using an expired
// role token
return (System.nanoTime() - cachedRoleTokenTimestamp) < TimeUnit.HOURS.toNanos(cacheDurationInHour);
return (System.nanoTime() - cachedRoleTokenTimestamp) < TimeUnit.MINUTES.toNanos(cacheDurationInMinutes);
}

@Override
public void configure(String encodedAuthParamString) {

if (isBlank(encodedAuthParamString)) {
throw new IllegalArgumentException("authParams must not be empty");
}
checkArgument(isNotBlank(encodedAuthParamString), "authParams must not be empty");

try {
setAuthParams(AuthenticationUtil.configureFromJsonString(encodedAuthParamString));
Expand All @@ -145,19 +154,31 @@ private void setAuthParams(Map<String, String> authParams) {
this.tenantDomain = authParams.get("tenantDomain");
this.tenantService = authParams.get("tenantService");
this.providerDomain = authParams.get("providerDomain");
// privateKeyPath is deprecated, this is for compatibility
if (isBlank(authParams.get("privateKey")) && isNotBlank(authParams.get("privateKeyPath"))) {
this.privateKey = loadPrivateKey(authParams.get("privateKeyPath"));
this.keyId = authParams.getOrDefault("keyId", "0");
this.autoPrefetchEnabled = Boolean.parseBoolean(authParams.getOrDefault("autoPrefetchEnabled", "false"));

if (isNotBlank(authParams.get("x509CertChain"))) {
// When using Copper Argos
checkRequiredParams(authParams, "privateKey", "caCert", "providerDomain");
// Absolute paths are required to generate a key refresher, so if these are relative paths, convert them
this.x509CertChainPath = getAbsolutePathFromUrl(authParams.get("x509CertChain"));
this.privateKeyPath = getAbsolutePathFromUrl(authParams.get("privateKey"));
this.caCertPath = getAbsolutePathFromUrl(authParams.get("caCert"));
} else {
this.privateKey = loadPrivateKey(authParams.get("privateKey"));
}
checkRequiredParams(authParams, "tenantDomain", "tenantService", "providerDomain");

if (this.privateKey == null) {
throw new IllegalArgumentException("Failed to load private key from privateKey or privateKeyPath field");
}
// privateKeyPath is deprecated, this is for compatibility
if (isBlank(authParams.get("privateKey")) && isNotBlank(authParams.get("privateKeyPath"))) {
this.privateKey = loadPrivateKey(authParams.get("privateKeyPath"));
} else {
this.privateKey = loadPrivateKey(authParams.get("privateKey"));
}

this.keyId = authParams.getOrDefault("keyId", "0");
this.autoPrefetchEnabled = Boolean.parseBoolean(authParams.getOrDefault("autoPrefetchEnabled", "false"));
if (this.privateKey == null) {
throw new IllegalArgumentException(
"Failed to load private key from privateKey or privateKeyPath field");
}
}

if (isNotBlank(authParams.get("athenzConfPath"))) {
System.setProperty("athenz.athenz_conf", authParams.get("athenzConfPath"));
Expand All @@ -183,19 +204,52 @@ public void close() throws IOException {
if (ztsClient != null) {
ztsClient.close();
}
if (keyRefresher != null) {
keyRefresher.shutdown();
}
}

private ZTSClient getZtsClient() {
private ZTSClient getZtsClient() throws InterruptedException, IOException, KeyRefresherException {
if (ztsClient == null) {
ServiceIdentityProvider siaProvider = new SimpleServiceIdentityProvider(tenantDomain, tenantService,
privateKey, keyId);
ztsClient = new ZTSClient(ztsUrl, tenantDomain, tenantService, siaProvider);
if (x509CertChainPath != null) {
// When using Copper Argos
if (keyRefresher == null) {
keyRefresher = Utils.generateKeyRefresherFromCaCert(caCertPath, x509CertChainPath, privateKeyPath);
keyRefresher.startup(retryFrequencyInMillis);
}
final SSLContext sslContext = Utils.buildSSLContext(keyRefresher.getKeyManagerProxy(),
keyRefresher.getTrustManagerProxy());
ztsClient = new ZTSClient(ztsUrl, sslContext);
} else {
ServiceIdentityProvider siaProvider = new SimpleServiceIdentityProvider(tenantDomain, tenantService,
privateKey, keyId);
ztsClient = new ZTSClient(ztsUrl, tenantDomain, tenantService, siaProvider);
}
ztsClient.setPrefetchAutoEnable(this.autoPrefetchEnabled);
}
return ztsClient;
}

private PrivateKey loadPrivateKey(String privateKeyURL) {
private static void checkRequiredParams(Map<String, String> authParams, String... requiredParams) {
for (String param : requiredParams) {
checkArgument(isNotBlank(authParams.get(param)), "Missing required parameter: %s", param);
}
}

private static String getAbsolutePathFromUrl(String urlString) {
try {
java.net.URL url = new URL(urlString).openConnection().getURL();
checkArgument("file".equals(url.getProtocol()), "Unsupported protocol: %s", url.getProtocol());
Path path = Paths.get(url.getPath());
return path.isAbsolute() ? path.toString() : path.toAbsolutePath().toString();
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid URL format", e);
} catch (InstantiationException | IllegalAccessException | IOException e) {
throw new IllegalArgumentException("Cannnot get absolute path from specified URL", e);
}
}

private static PrivateKey loadPrivateKey(String privateKeyURL) {
PrivateKey privateKey = null;
try {
URLConnection urlConnection = new URL(privateKeyURL).openConnection();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
import org.testng.annotations.Test;
import org.apache.pulsar.common.util.ObjectMapperFactory;
import static org.apache.pulsar.common.util.Codec.encode;
import org.testng.Assert;
import org.testng.annotations.BeforeClass;

import java.io.File;
Expand All @@ -45,6 +45,8 @@
import com.yahoo.athenz.zts.RoleToken;
import com.yahoo.athenz.zts.ZTSClient;

import lombok.Cleanup;

public class AuthenticationAthenzTest {

private AuthenticationAthenz auth;
Expand Down Expand Up @@ -144,7 +146,7 @@ public void testLoadPrivateKeyBase64() throws Exception {
PrivateKey key = (PrivateKey) field.get(authBase64);
assertEquals(key, privateKey);
} catch (Exception e) {
Assert.fail();
fail();
}
}

Expand All @@ -171,8 +173,70 @@ public void testLoadPrivateKeyUrlEncode() throws Exception {
PrivateKey key = (PrivateKey) field.get(authEncode);
assertEquals(key, privateKey);
} catch (Exception e) {
Assert.fail();
fail();
}
}

@Test
public void testCopperArgos() throws Exception {
@Cleanup
AuthenticationAthenz caAuth = new AuthenticationAthenz();
Field ztsClientField = caAuth.getClass().getDeclaredField("ztsClient");
ztsClientField.setAccessible(true);
ztsClientField.set(caAuth, new MockZTSClient("dummy"));

ObjectMapper jsonMapper = ObjectMapperFactory.create();
Map<String, String> authParamsMap = new HashMap<>();
authParamsMap.put("providerDomain", "test_provider");
authParamsMap.put("ztsUrl", "https://localhost:4443/");

try {
caAuth.configure(jsonMapper.writeValueAsString(authParamsMap));
fail("Should not succeed if some required parameters are missing");
} catch (Exception e) {
assertTrue(e instanceof IllegalArgumentException);
}

authParamsMap.put("x509CertChain", "data:application/x-pem-file;base64,aW52YWxpZAo=");
try {
caAuth.configure(jsonMapper.writeValueAsString(authParamsMap));
fail("'data' scheme url should not be accepted");
} catch (Exception e) {
assertTrue(e instanceof IllegalArgumentException);
}

authParamsMap.put("x509CertChain", "file:./src/test/resources/copper_argos_client.crt");
try {
caAuth.configure(jsonMapper.writeValueAsString(authParamsMap));
fail("Should not succeed if 'privateKey' or 'caCert' is missing");
} catch (Exception e) {
assertTrue(e instanceof IllegalArgumentException);
}

authParamsMap.put("privateKey", "./src/test/resources/copper_argos_client.key");
authParamsMap.put("caCert", "./src/test/resources/copper_argos_ca.crt");
caAuth.configure(jsonMapper.writeValueAsString(authParamsMap));

Field x509CertChainPathField = caAuth.getClass().getDeclaredField("x509CertChainPath");
x509CertChainPathField.setAccessible(true);
String actualX509CertChainPath = (String) x509CertChainPathField.get(caAuth);
assertFalse(actualX509CertChainPath.startsWith("file:"));
assertFalse(actualX509CertChainPath.startsWith("./"));
assertTrue(actualX509CertChainPath.endsWith("/src/test/resources/copper_argos_client.crt"));

Field privateKeyPathField = caAuth.getClass().getDeclaredField("privateKeyPath");
privateKeyPathField.setAccessible(true);
String actualPrivateKeyPath = (String) privateKeyPathField.get(caAuth);
assertFalse(actualPrivateKeyPath.startsWith("file:"));
assertFalse(actualPrivateKeyPath.startsWith("./"));
assertTrue(actualPrivateKeyPath.endsWith("/src/test/resources/copper_argos_client.key"));

Field caCertPathField = caAuth.getClass().getDeclaredField("caCertPath");
caCertPathField.setAccessible(true);
String actualCaCertPath = (String) caCertPathField.get(caAuth);
assertFalse(actualCaCertPath.startsWith("file:"));
assertFalse(actualCaCertPath.startsWith("./"));
assertTrue(actualCaCertPath.endsWith("/src/test/resources/copper_argos_ca.crt"));
}

@Test
Expand Down
20 changes: 20 additions & 0 deletions pulsar-client-auth-athenz/src/test/resources/copper_argos_ca.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDPDCCAiQCCQCmRd+BE+zjnTANBgkqhkiG9w0BAQsFADBfMQswCQYDVQQGEwJK
UDEOMAwGA1UECAwFVG9reW8xFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UE
CgwTRGVmYXVsdCBDb21wYW55IEx0ZDELMAkGA1UEAwwCY2EwIBcNMjMwMjAzMDgz
MTMxWhgPMjA1MzAxMjYwODMxMzFaMF8xCzAJBgNVBAYTAkpQMQ4wDAYDVQQIDAVU
b2t5bzEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENv
bXBhbnkgTHRkMQswCQYDVQQDDAJjYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAOzLMnX8MaKB5vlqQo1Ur07GpoiG2G1fdJIyId8dCiJsBP7QoefUpXRU
65iOcv9qCVcWl/1K489/FNNPeRV5TMUCjQlySJWDMtzTGZV+YCLTGxtQde+4JQOo
v342VZx8tuAQ6LNbg4pygZFiQBhTzkbwzgj/NgKqXNk6RzqI/EUpPVD+PWXEOG+U
X33S/YeagqJ7ISy0Ek/Z/jOwYe/uQbSTlSNh30AwN4W1M4/l0tJmyGWQZkouGjHV
uJpHCGyMardX2XKQRo85HqDY+VxD7sFVe2XM3cYe86PY0W/6mTaFXFBoo0Wvh71e
GrbaJ3dfxLL3jaSahaNh6H5fXOlamxMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA
CPCP9QdeNk+1VarLKd9AiHLXzphwhpzsaqZ2AyRYNGgP9T5+DfY4+WPdLj0M9M6l
DNFeGH0LxOBnVlpIRBRc/4FULkZofuzWaGpSvtGlgJbuE4aQALv0L6UIt808BMrC
7EW9h4nABgrUfkyFXc8QSoRCrL1QM4cmpWOU3rcgX7JElhGVwljrOfRutK1vw8LD
pvlWAUr5stUohTe7rsuC/PGIaf2fBtsbtXSntF0oqEFcN8JNkHph+kRaiQLiq6qE
iStPJGqk95fpP/IZiiCULXREqRSYj6KM/9Ll0bmvysb/LQBg0s2PL71yr8qS+htG
Y173Y2JCrv2IWyq28Tcj7A==
-----END CERTIFICATE-----
27 changes: 27 additions & 0 deletions pulsar-client-auth-athenz/src/test/resources/copper_argos_ca.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA7MsydfwxooHm+WpCjVSvTsamiIbYbV90kjIh3x0KImwE/tCh
59SldFTrmI5y/2oJVxaX/Urjz38U0095FXlMxQKNCXJIlYMy3NMZlX5gItMbG1B1
77glA6i/fjZVnHy24BDos1uDinKBkWJAGFPORvDOCP82Aqpc2TpHOoj8RSk9UP49
ZcQ4b5RffdL9h5qConshLLQST9n+M7Bh7+5BtJOVI2HfQDA3hbUzj+XS0mbIZZBm
Si4aMdW4mkcIbIxqt1fZcpBGjzkeoNj5XEPuwVV7Zczdxh7zo9jRb/qZNoVcUGij
Ra+HvV4attond1/EsveNpJqFo2Hofl9c6VqbEwIDAQABAoIBAQC7aqyaw6wJYmWM
3TSlpfRHFmWyw3/DOX0LRVCXxeVCj1p40GqFEkKOS7RY/843KBcSbdiIauDaV0wF
X+6HN4WynK1CX8jhRYFZVF/4eZjfl1TqDon53Ta2qbY+0AR8oh0gRWHYq8L2LmEs
z6XJW3N1pJx+dHisLWjlqgG8a7W3ikGIKyvS2dzZf4qK1QfXcpf0sjyuxdcdlqZ2
ZhCaEJXamnHj0srY7KF/eAV1S0WBuVfxdwDPwZRa0nOgbbw8G+2q6bW4Sd/qg3GY
gYT40ocdoAh4lCWwTVXOGl4Y4i+VohmG+FYjCEqCgJawIkgn9avOPaQyfJb/BBqm
lEdKij8RAoGBAP3NcmSdyfAsRpAtF++RMVtuqMln+dKg/nrCcDwYgFNlwdiBCdf4
b2bJ3LokySu5004jf7JhtnNRth3RXDpiOoeBJ1uJX86U+I3t1XQr/JuYdNuxKazA
zYlGshNQGXWrMhj2XsSNnF5KKIXy+g9vF8IHg15Kew8NiCegEdvYqAELAoGBAO7Y
DOA39OoEKNbR9EvGauSwDDKeyYJi/3E8WekCFFwzpNkBiG+JD1bTcFWe9/q10O3L
fAyvkO9ZQwh3za2hfixb3SGTdHdsQZ7wG4X3eHnbVTwxFV0mct8aDgfRRzQyaDpA
KdDYEsd64hvuHKsnLqOcaKYie6/qfagywZz3KiMZAoGAGF/gupUEzdISvMn34IQb
L2LDRwR7U6Uui2+dA8h+moPNSBOsdFdhq4d7cU0THOXtyzVRkDoeIZkZWme+6cSB
Rn4632mkD9zyuf67XzrSOcc8gdTT4clqc+KcO4qXx1s3pnoSw+GtwMhyd9rL9SuA
Jpw+G5Ifm2R7TQLsdCasi90CgYEAnbgjwIiS/Vmj0j+wr70l5z/tvhum+6f+ALuW
r8yEv2IHEJn3i5eZfn9/ZbrlDDS18+F0WDgzYCq0nknmkyraU9aRztM9jIL7TkZG
FpAViXpx7Z6H+gwivPrKmxTyjSBgPV8TferBc+LMnx785XSpUrc9T7/jp4YUVla2
Db4VoDkCgYAvu6/m8VsbdY/+hiDNIgLtVw0hxtw4GKk6r+MT237MtaamDPv61hdA
r6QNXTMXmO4rK0BihD0l/OB2s/wHc2o393Utyv9gmoRL6owzqZGwCmrpJIPH0zqZ
6Bs8S0eMH5djc9i9qzmGa02eSB6m4B6GHbG1n6b1uBKj3CXeRyZf6w==
-----END RSA PRIVATE KEY-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEADCCAuigAwIBAgIBATANBgkqhkiG9w0BAQsFADBfMQswCQYDVQQGEwJKUDEO
MAwGA1UECAwFVG9reW8xFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UECgwT
RGVmYXVsdCBDb21wYW55IEx0ZDELMAkGA1UEAwwCY2EwIBcNMjMwMjAzMDg1NDQw
WhgPMjA1MzAxMjYwODU0NDBaMGYxCzAJBgNVBAYTAkpQMQ4wDAYDVQQIDAVUb2t5
bzEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBh
bnkgTHRkMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQCU8DAoUhKDnuhhOCcAwKBDAUFn76zngfM8+uvAbRg+8ioklrHJ
eyAwp7mv9yx0BMwwjs74pkpMFesEmHrvXTYl77lvmsxWcOU2AK0c59O/GyMJkwAm
peXAK3oFUxrYdDtlIFEEtfc/7IcGwAX6D2OKKuBTwY1gRgIH6kGXKc1Udg9DFWmi
q8rWumj4KCtdlULPTsB2siqQNfluzb91rXpQscds2RiF3lcKPdn8b2V09C6320/0
Yhl010ufAfp3Qi8ki3JLx0zAMgh/JnI92NeGBNutOlxK53rIPHE1iujys1nQ0adH
Ufqw6aSPsKb7Q45zA01/rNx93QkIgAU1QEyfAgMBAAGjgb0wgbowCQYDVR0TBAIw
ADATBgNVHSUEDDAKBggrBgEFBQcDAjAdBgNVHQ4EFgQU8xZtnrn3ZD2kUEr+GjGQ
GWqHqf0weQYDVR0jBHIwcKFjpGEwXzELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRv
a3lvMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQgQ29t
cGFueSBMdGQxCzAJBgNVBAMMAmNhggkApkXfgRPs450wDQYJKoZIhvcNAQELBQAD
ggEBAKM1pEqOD4WWXIu1lJeXJgIeH2JpEZYbuGzHABLWBCQpfOP/6H0olaUVgh8H
/tln3r+9xU6fwCJby/uEQ/0VAflhapanMVL85bDnLQ/Y7bCcM5peZKRak3x9GpOZ
xBPGJcC2P5XgNG+Uaewr48rL7lv71idWl7hmai6pfI50vjEwjePoeYP0ohtEFzoN
3txefESn5DEjvyw51vn+hWh/E9NNLTgc19GO6KjUCAkc9nq10ylYKk0NBIu6z+Dd
Kb9p+i70L/AeVIq4NPsda6S7XxDkluHaKZI4sDC0PSz9/R3Tez9ECuXzkLbWShpJ
zZbWVMeX/SRdK4RmF5yvxfCtP1Q=
-----END CERTIFICATE-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAlPAwKFISg57oYTgnAMCgQwFBZ++s54HzPPrrwG0YPvIqJJax
yXsgMKe5r/csdATMMI7O+KZKTBXrBJh67102Je+5b5rMVnDlNgCtHOfTvxsjCZMA
JqXlwCt6BVMa2HQ7ZSBRBLX3P+yHBsAF+g9jiirgU8GNYEYCB+pBlynNVHYPQxVp
oqvK1rpo+CgrXZVCz07AdrIqkDX5bs2/da16ULHHbNkYhd5XCj3Z/G9ldPQut9tP
9GIZdNdLnwH6d0IvJItyS8dMwDIIfyZyPdjXhgTbrTpcSud6yDxxNYro8rNZ0NGn
R1H6sOmkj7Cm+0OOcwNNf6zcfd0JCIAFNUBMnwIDAQABAoIBAGqnkaTeGPn+SqSM
BIoqZtl0xbS7UpM6YMgTW82hkhJJclpfO5Nvw350LanQFBpE8T/4lEhFNMFFlNXm
p2pP0p3aDG3aaWehUtKYK1+et+iLc0zA4wPKGzvBJpE3kOreWUYynTIFaLhzFcKE
sgL/ECX6TEhOO4Jsv7mRTEUGn05SYTOrdgWoTY8eGOFZGe35WnBEUHhpjvbvllwE
TEq3OO0A8SF7BbXp7KRZYnG0uKalwZsHExMgRfpHLD5tMTwVVP+bs4Ib+2QdEFWD
3gfcAMUzhlAbvlVbaxF51gmj4Leh0/XezTYoCAbdG9m6b8jhVs0CO69+hLqbEPyj
YG8W7IECgYEAw/f04XPnrjqP68oofwGJihwp+8WLsKI1P0nMF+8eJMNbSOG530w6
SCDp/pMC5Inklvz5Z13/Ak+H8m07FaU04xFI6SLFAYUTG1KFrvIgtZpJncJ/bJRb
rfa0sLNcHb7NbVlDjLkgoM0kuVdtjwq+znfL6vD0hd269RbfjTHm198CgYEAwpAR
H7CHZ2d4B3qKcqAX1DR9VYvP2gvcB+1MXKROwxQAbAnUwNqkSi6WVKI6uffW3d6F
QKHu2mingDhrI+SfXD/Ec9gDXwakmu8x2kGwC4cWo9TNMlqg2nTCv5yAs4rbGR3H
8NIkYOqFg3sgK0I97+7GEuFnL5RKtSVt4sngI0ECgYB0FRwstICnly8LqCuG2D1F
31sLNcCCeAN8otVP1CgR9NrM+FEnMbtQYJbbYvASuo/61I1UKrzU/JF2DDg0oTEL
1IBRAXSbat2fkKl5sRmpGWTEG6NpiRQpn3r3NLe7Mvvy6y51XHA0cHBxjZVrZx0R
pqrXV7Yw2eBWMB9qPwYUFwKBgE0QDxhELX2RiAM+UDQSoR2WJMaLeCpfZClnnkVb
dy7hb0Fbq38vmr8fMMAY+bXLKrn6d0EgYqDzrtSkhBtVZKF/SGqx9rPex7fuYgqW
1gna2ebOVPBK4Udl0/VdIcT7jMin+RezxGD2wydOz3ES7cFpC99SlDJORED3sEyR
tUuBAoGBAJwjHcH11jlNKH5XDMSIixBtnHzmlv0BlsKXO9E7fX1KLDi8UD3lGqef
SFUL/QGXlvQZh9QwXW/HCiMI0UPqGZPeYJFlX4DhgxkO23GR1kT4iKJsec27qUcH
ah0Ongww5kNPn5euiiO43+C9aX2ardBvR7tZQErGjaR62Br3wjEb
-----END RSA PRIVATE KEY-----