Skip to content

Commit

Permalink
[improve][client] AuthenticationAthenz supports Copper Argos (apache#…
Browse files Browse the repository at this point in the history
  • Loading branch information
Masahiro Sakamoto authored Feb 8, 2023
1 parent 6506f9b commit d7c4e37
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 27 deletions.
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-----

0 comments on commit d7c4e37

Please sign in to comment.