Skip to content

Commit

Permalink
Add test for OIDC truststore w/JKS to PEM conversion
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Edgar <[email protected]>
  • Loading branch information
MikeEdgar committed Jan 3, 2025
1 parent 4f3e5e1 commit e15b8a0
Show file tree
Hide file tree
Showing 4 changed files with 269 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ private boolean putTrustStoreValue(Map<String, String> data, String sourceName,
private void encodeCertificate(OutputStream buffer, Certificate certificate) throws IOException, CertificateEncodingException {
buffer.write("-----BEGIN CERTIFICATE-----\n".getBytes(StandardCharsets.UTF_8));
buffer.write(Base64.getMimeEncoder(80, new byte[] {'\n'}).encode(certificate.getEncoded()));
buffer.write("-----END CERTIFICATE-----\n".getBytes(StandardCharsets.UTF_8));
buffer.write("\n-----END CERTIFICATE-----\n".getBytes(StandardCharsets.UTF_8));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
package com.github.streamshub.console;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.util.Base64;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.junit.jupiter.api.Test;

import com.github.streamshub.console.api.v1alpha1.Console;
import com.github.streamshub.console.api.v1alpha1.ConsoleBuilder;
import com.github.streamshub.console.api.v1alpha1.spec.TrustStore;
import com.github.streamshub.console.api.v1alpha1.spec.security.Audit.Decision;
import com.github.streamshub.console.api.v1alpha1.spec.security.Rule;
import com.github.streamshub.console.api.v1alpha1.status.Condition;
import com.github.streamshub.console.config.security.Privilege;
import com.github.streamshub.console.dependents.ConsoleDeployment;
import com.github.streamshub.console.dependents.ConsoleResource;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.api.model.EnvVar;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.api.model.Volume;
import io.fabric8.kubernetes.api.model.VolumeMount;
import io.quarkus.test.junit.QuarkusTest;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

@QuarkusTest
class ConsoleReconcilerSecurityTest extends ConsoleReconcilerTestBase {
Expand Down Expand Up @@ -198,4 +219,130 @@ void testConsoleReconciliationWithMissingRules() {
assertEquals("security.roles[0].rules must not be empty", errorCondition.getMessage(), errorString);
});
}

@Test
void testConsoleReconciliationWithOidcTrustStore() throws Exception {
Secret passwordSecret = new SecretBuilder()
.withNewMetadata()
.withName("my-secret")
.withNamespace("ns2")
.addToLabels(ConsoleResource.MANAGEMENT_LABEL)
.endMetadata()
.addToData("pass", Base64.getEncoder().encodeToString("changeit".getBytes()))
.build();

client.resource(passwordSecret).create();

try (InputStream in = getClass().getResourceAsStream("kube-certs.jks")) {
byte[] truststore = in.readAllBytes();

ConfigMap contentConfigMap = new ConfigMapBuilder()
.withNewMetadata()
.withName("my-configmap")
.withNamespace(CONSOLE_NS)
.addToLabels(ConsoleResource.MANAGEMENT_LABEL)
.endMetadata()
.addToBinaryData("truststore", Base64.getEncoder().encodeToString(truststore))
.build();
client.resource(contentConfigMap).create();
}

Console consoleCR = new ConsoleBuilder()
.withMetadata(new ObjectMetaBuilder()
.withName(CONSOLE_NAME)
.withNamespace(CONSOLE_NS)
.build())
.withNewSpec()
.withHostname("example.com")
.withNewSecurity()
.withNewOidc()
.withAuthServerUrl("https://example.com/.well-known/openid-connect")
.withIssuer("https://example.com")
.withClientId("client-id")
.withClientSecret("client-secret")
.withNewTrustStore()
.withType(TrustStore.Type.JKS)
.withNewPassword()
.withNewValueFrom()
.withNewSecretKeyRef("pass", "my-secret", Boolean.FALSE)
.endValueFrom()
.endPassword()
.withNewContent()
.withNewValueFrom()
.withNewConfigMapKeyRef("truststore", "my-configmap", Boolean.FALSE)
.endValueFrom()
.endContent()
.endTrustStore()
.endOidc()
.endSecurity()
.addNewKafkaCluster()
.withName(kafkaCR.getMetadata().getName())
.withNamespace(kafkaCR.getMetadata().getNamespace())
.withListener(kafkaCR.getSpec().getKafka().getListeners().get(0).getName())
.endKafkaCluster()
.endSpec()
.build();

client.resource(consoleCR).create();

awaitDependentsNotReady(consoleCR, "ConsoleIngress");
setConsoleIngressReady(consoleCR);
awaitDependentsNotReady(consoleCR, "ConsoleDeployment");
var consoleDeployment = setDeploymentReady(consoleCR, ConsoleDeployment.NAME);

var podSpec = consoleDeployment.getSpec().getTemplate().getSpec();
var containerSpecAPI = podSpec.getContainers().get(0);

var volumes = podSpec.getVolumes().stream().collect(Collectors.toMap(Volume::getName, Function.identity()));
assertEquals(3, volumes.size()); // cache, config + 1 volume for truststore

var truststoreVolName = "oidc-truststore-trust";

var truststoreVolume = volumes.get(truststoreVolName);
assertEquals("oidc-truststore.trust.content", truststoreVolume.getSecret().getItems().get(0).getKey());
assertEquals("oidc-truststore.trust.jks", truststoreVolume.getSecret().getItems().get(0).getPath());

var mounts = containerSpecAPI.getVolumeMounts().stream().collect(Collectors.toMap(VolumeMount::getName, Function.identity()));
assertEquals(3, mounts.size(), mounts::toString);

var truststoreMount = mounts.get(truststoreVolName);
var truststoreMountPath = "/etc/ssl/oidc-truststore.trust.jks";
assertEquals(truststoreMountPath, truststoreMount.getMountPath());
assertEquals("oidc-truststore.trust.jks", truststoreMount.getSubPath());

var envVarsAPI = containerSpecAPI.getEnv().stream().collect(Collectors.toMap(EnvVar::getName, Function.identity()));

var truststorePath = envVarsAPI.get("QUARKUS_TLS__OIDC_PROVIDER_TRUST__TRUST_STORE_JKS_PATH");
assertEquals(truststoreMountPath, truststorePath.getValue());
var truststorePasswordSource = envVarsAPI.get("QUARKUS_TLS__OIDC_PROVIDER_TRUST__TRUST_STORE_JKS_PASSWORD");
assertEquals("console-1-console-secret", truststorePasswordSource.getValueFrom().getSecretKeyRef().getName());
assertEquals("oidc-truststore.trust.password", truststorePasswordSource.getValueFrom().getSecretKeyRef().getKey());

var containerSpecUI = podSpec.getContainers().get(1);
var envVarsUI = containerSpecUI.getEnv().stream().collect(Collectors.toMap(EnvVar::getName, Function.identity()));
var truststorePemRef = envVarsUI.get("CONSOLE_SECURITY_OIDC_TRUSTSTORE").getValueFrom().getSecretKeyRef();
var truststorePemSecret = client.resources(Secret.class)
.inNamespace(CONSOLE_NS)
.withName(truststorePemRef.getName())
.get();
var truststorePemValue = Base64.getDecoder().decode(truststorePemSecret.getData().get(truststorePemRef.getKey()));

CertificateFactory fact = CertificateFactory.getInstance("X.509");
Collection<? extends Certificate> expectedCertificates;
Collection<? extends Certificate> actualCertificates;

try (InputStream in = getClass().getResourceAsStream("kube-certs.pem")) {
expectedCertificates = fact.generateCertificates(in);
}

try (InputStream in = new ByteArrayInputStream(truststorePemValue)) {
actualCertificates = fact.generateCertificates(in);
}

assertEquals(expectedCertificates.size(), actualCertificates.size());

for (Certificate exp : expectedCertificates) {
assertTrue(actualCertificates.contains(exp));
}
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
-----BEGIN CERTIFICATE-----
MIIDMjCCAhqgAwIBAgIIMYTYDdowPTswDQYJKoZIhvcNAQELBQAwNzESMBAGA1UE
CxMJb3BlbnNoaWZ0MSEwHwYDVQQDExhrdWJlLWFwaXNlcnZlci1sYi1zaWduZXIw
HhcNMjQxMDIxMDc1NjU5WhcNMzQxMDE5MDc1NjU5WjA3MRIwEAYDVQQLEwlvcGVu
c2hpZnQxITAfBgNVBAMTGGt1YmUtYXBpc2VydmVyLWxiLXNpZ25lcjCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBALnWRIe7l/94zj624Ax8lGDdfoppPO9i
EtnkBBjAhkfe3ChnL33b+edeGf9lfIxZYbMVng+tEZhq2RHrp40ZSA1BZ74TwTaQ
1FfaSLU1dMNIWvgudNQMcgDNXTxXRamup5/wZ5udKYUBLVPvFEvmJ+je9QCwEGKQ
JrpDX+aKJOLPKyxVox6ZcqBTKJts+/f6fEqrbDwdlQhGAZRSsfXYZgufSSvRO7gN
67tp3KJ9OhuEzvkMoSFvvQxPmxVlWGKdkZFdKNU1vSI0aOBLlCWHOBmk+Wvu/fTi
iuBybWooaQIvu3CSEXGSJaQ+b4Ol/Uj0EO70Q58HzFndst6wxwD+ZGECAwEAAaNC
MEAwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFAj8
c+01Kl+BDcIolkavVzAU1G41MA0GCSqGSIb3DQEBCwUAA4IBAQBL97dVCrkgo6ca
7/4nZoP1i7owpWV0hfJWU/TKSFBa7Vzbe7xWyT/HBzjPikoUwZEpH7rZAcHwcYQr
tTozW/zDOZS98cnrepY/tAXVi7Hz5wnuaguI3iwFaIVh9OR8FBZ5TAMaXGW1mYEg
q0jNZY5cbFm+3bacRKSF//hS/3nms3o3b6uni2f4rZGED4iW+zK2qXZfz+B/uCwA
1KoHt3TxZsJ8scVXCMJQi7T0cdjR9pGucCRVFoXKxGE0sIL28ajBdOAIalTh/Wh4
F6n73xM2Ao84Qi1qk6HrWJHQb1DY/L/+tZc5TlEHbAZwHR3Y8nYpxTlvNYTcuft+
dGv8WhYX
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDQDCCAiigAwIBAgIINykiDqNyBqwwDQYJKoZIhvcNAQELBQAwPjESMBAGA1UE
CxMJb3BlbnNoaWZ0MSgwJgYDVQQDEx9rdWJlLWFwaXNlcnZlci1sb2NhbGhvc3Qt
c2lnbmVyMB4XDTI0MTAyMTA3NTY1OFoXDTM0MTAxOTA3NTY1OFowPjESMBAGA1UE
CxMJb3BlbnNoaWZ0MSgwJgYDVQQDEx9rdWJlLWFwaXNlcnZlci1sb2NhbGhvc3Qt
c2lnbmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw7zTqYxszydv
LjATLq9ca+rf9URZfiBhKSVWFbM7tRH/gUVsdTPsmq35FfUKFCn6T9UHT+rgOGKA
Z+PqMoDAMOe2QAYPdPUGPHgy5Op2iLfFPagUuOA28avqdryRUXfjMqZpx0EEg6kp
X9O5nOfVKBNYdSlWB5ZGvWl4rUuuUyU+OrzDnyvozRrEvbUt0bLMX4JdYT3u7mlB
MAP+UXKg3qxes3huHP7PSbXRGCV2o5zZzmy0WSxPx2xg0BN3DQnIqxYx8o2w0A8O
Cy1tZAduQZWP33uluFMubRCws3pjRsIUJozhM+POjbEI3e2nmwPRmIuwkJ+u3gYV
58tlkahuBwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAqQwDwYDVR0TAQH/BAUwAwEB
/zAdBgNVHQ4EFgQUSCtp8dg6T1kO1V7KFkeP6Ioo7RYwDQYJKoZIhvcNAQELBQAD
ggEBACTBDHiGLxRzkT++oWh8n9aYVo9kmq3pCy5xyh0vDGY5YublnrLIX0bf8aL9
rpGrs9WKqi2z/1HtUm6XNJP9eO+Vt08FUP63RSkPQpU+w4qwbsKqu6frbyyIddpK
caUDWA1ggor3aV7umz2F4n6wVg2HDKDBrR/JHeEpWFOeXX2KeDFkomTot93RtCkd
yo6Y/6PYVFfj+SW9rI+b1WQ9U0BqRjgFzRPzp9wryx6c8n05mTcH3C6vxwrifr8v
NLaxI+xVaMtLALJ6pK1lVAPOkS0J5JaewZqOCXySTf2lKn6KC0xJYC0vHdKf27oi
UU33fkByf2O1XayqoEfCXjZ6uPA=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDTDCCAjSgAwIBAgIIJM+hLK9EGE0wDQYJKoZIhvcNAQELBQAwRDESMBAGA1UE
CxMJb3BlbnNoaWZ0MS4wLAYDVQQDEyVrdWJlLWFwaXNlcnZlci1zZXJ2aWNlLW5l
dHdvcmstc2lnbmVyMB4XDTI0MTAyMTA3NTY1OVoXDTM0MTAxOTA3NTY1OVowRDES
MBAGA1UECxMJb3BlbnNoaWZ0MS4wLAYDVQQDEyVrdWJlLWFwaXNlcnZlci1zZXJ2
aWNlLW5ldHdvcmstc2lnbmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEAoSQzZgP2w02xuh7zlmzAN4hYg2Saz9NMescrUlfCCtqNIVJPCCTx45m2tbhF
jUNv84zjtrJ81BOugyEGQchdjJGoKUdGNcpVUC1Ts/jrRnyuVrmvifjgRl1lFi6u
l5G0jGsgv7Z2W1JT5EpIfaA0qlsUnecdQtm7qienUeWRO9HfYJ08eNaUF+zAB6JA
i8I6AzTl6rJT33EmPymNpXrHFVgr/IDHs2jFakjTauPIGScRxFRze0JTMSvlz/8j
YIu6g59THds1ROm2+NYblcES3zuZeHQ9n4iRalIH8pQ8LQ9lQJ79S4dkYYo++Klu
W60erjZt20zdMgIKnwTtnAY0mQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAqQwDwYD
VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUMWWSZ6Bg1Rs/dGd+Yvl4m05aDowwDQYJ
KoZIhvcNAQELBQADggEBAEc3IMR1duWEuwIqNQC42TX5upKEAM/d065A4kjiLn3k
ACyFoB7gqA55fy23kTDsAPqcRPeSvwJstUOxIq0eP1q+HUFbdPXa2ORiTmPihY/u
oagSkEMQvSi6vTFKl2wTrdpHZzfk1FgYn6kLwX8LYXyHziS6uRxQHPIvUmgCIs6I
MeHWgJh2rKhX37YD6aW7uv2waif5qs6/pUhDqVoafqmvXp4FMNBLbA6JDE8PcVHA
rFHOvhQJ0FtT4VPwll9/VR9aE7pWBZA6fwvbThWB8WILFGsXbF3bcuR0fu1WZte0
RtkV+Ps4/zb9TZAkdy1TcIRxvSxVrarHUJRNdtCZfUk=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDlzCCAn+gAwIBAgIIK6oSarI03tkwDQYJKoZIhvcNAQELBQAwWTFXMFUGA1UE
AwxOb3BlbnNoaWZ0LWt1YmUtYXBpc2VydmVyLW9wZXJhdG9yX2xvY2FsaG9zdC1y
ZWNvdmVyeS1zZXJ2aW5nLXNpZ25lckAxNzI5NDk4Njk0MB4XDTI0MTAyMTA4MTgx
NFoXDTM0MTAxOTA4MTgxNVowWTFXMFUGA1UEAwxOb3BlbnNoaWZ0LWt1YmUtYXBp
c2VydmVyLW9wZXJhdG9yX2xvY2FsaG9zdC1yZWNvdmVyeS1zZXJ2aW5nLXNpZ25l
ckAxNzI5NDk4Njk0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyZPM
58DFXDJfatBHxoxJY2gpSI3rSxF7RHxmIghQxVQQHMD2ny4gkuflyrUYO2VnVZ+v
RfLxl0lhTdx0hLiKwniWqt6rmj+7/0oVbEW2cbWd+OVDos841LxhLZOwrHh+WcQW
spH3NgczP846uPg/yAKEUWX0xAej0lfD1//qr+VdUbhtx7xDl42Jzzt/Me9WS1Lh
J+tHU8Ooa+U2yTX/mGjQgwxfB4qoczoXvpvv1hO35g/wHiKeuOvUCmgzYrPQPCFK
3lx3ETKzC1m+MiRqYYqMDW4DCD43khSTF7XAbFjSMKqk6KrOi4xqhmjyTRZDyX7m
wRun84930lP3/U1ZgQIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAqQwDwYDVR0TAQH/
BAUwAwEB/zAdBgNVHQ4EFgQU6ZTgKTS9WAZ8+8OZZvy5Xu9wMnwwHwYDVR0jBBgw
FoAU6ZTgKTS9WAZ8+8OZZvy5Xu9wMnwwDQYJKoZIhvcNAQELBQADggEBAFO6LKg7
ILwywt/52kbfPRrEvpb5p3T4ANs1c50sU0YewbvT1phhbX0xG63kNm6isuZSLCie
7aNLDuEAjv4HmY4QffGvKHgyIQsII8+/W7JmS+nRgPEI6Yj3tJmy3gvN3X0xrBdt
S96+jCag1aR58zJ9imRaZOBNNlE4aedbvllFZ2k4Gk4BSZjqSJhNZSPaZmWUNsAH
nq/t16ZKs43aLtwqBRTI3ssGmcZjMTNeFVXVV0/WmjIRAnBvJHipzwZvZEyqw+EL
aJIz1fio6X1uGPyIBr4sp/p7Do0eUc+euEi1kLmctsDmntfJzD/WJLDh1qf7t/Ko
5NspdFu8g+UVpsg=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDWzCCAkOgAwIBAgIIIO4v2+5yCt0wDQYJKoZIhvcNAQELBQAwJjEkMCIGA1UE
AwwbaW5ncmVzcy1vcGVyYXRvckAxNzI5NDk4Mjk0MB4XDTI0MTAyMTA4MTEzNVoX
DTI2MTAyMTA4MTEzNlowHTEbMBkGA1UEAwwSKi5hcHBzLWNyYy50ZXN0aW5nMIIB
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA46vu506RtAKNTOemgaD1kryD
x0QzhjqyaS6XLUhiEpimfaSl1erPJJyjpvPylZpXMPqodQbTVaCOhbmNPFOFygEL
sfB1mpIGKNokTtm2mHE+YhfMLhAOb0OuSAEybk3EqZGxLrUQwVp88owYJHe3bPAG
wG6mMR6Sn/eL3shxpWfgpmVYnATyt2/7qpdTequxyOhasr2KIxD4ScvCwzycgTGS
xqEV2rSczmyZRWNdyw8p+V8394Uow5r7W9s2mUYC/KF6fvAs2RavKtSFD7ZLoBBW
xJmkzrT1uq5duOGgadnKZBo3IMj45zG8PooZ3cDepAVk3+vG3MfTpWHvZFOdywID
AQABo4GVMIGSMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAM
BgNVHRMBAf8EAjAAMB0GA1UdDgQWBBS82jT8YPTXx+3VZ4i5DEfntaJzxzAfBgNV
HSMEGDAWgBTp+7an7Jz7tRjmpnBveqo1hpNIsTAdBgNVHREEFjAUghIqLmFwcHMt
Y3JjLnRlc3RpbmcwDQYJKoZIhvcNAQELBQADggEBAFZ7O3aoPG+OMnVuLlg6S6/I
d1UeyBLdftVWzeB2Y0yzCb6PCPyuj1CVT7k64mqFLCYp/HT7E5QgmsFUDz62Q0Os
bLhKoRcBnPZnk2m58QQf8h4rzvc1oEgrhhYP3KW+RLfmwfAIVLXDzhXhEOejaC+B
mkvYhRsYCW7X9hc/+UrhCWpLFXASXTtJIitbNRHVFfgRMm71mh9NhwFRAQBexwO6
0ZG0MKapIaYWVknGrNPDTauoXaxyX6WQxM3VqLujCzHKz2OQmsylIcCXG/xtUfhU
JjBImOIsbWvvE3KWfPCA+rXg1MzoUagmECfjkq9uABqooIUxxksCRTb1wqj3DW0=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDDDCCAfSgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtpbmdy
ZXNzLW9wZXJhdG9yQDE3Mjk0OTgyOTQwHhcNMjQxMDIxMDgxMTMzWhcNMjYxMDIx
MDgxMTM0WjAmMSQwIgYDVQQDDBtpbmdyZXNzLW9wZXJhdG9yQDE3Mjk0OTgyOTQw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpCyfKVGuk6af3hAdOqaPi
cEpF3NHMhZDfBHcnCSVz5U7NiBVkDBuYoQbHrFFNnnAO1TWH/ztwLtjH8odUelWa
84Ue7xpDOrCDgNBIEsB7ymoV4oyRw3DVuuC9kfAWx6+YhuP6hEltOVwJvXdC50A7
SKQTuDUSjF7VZF1RXQW8CBJO+2/cwuXhC+O9z3VHuSTzAUFDsycTnVwC8uB7Ycn/
7t/UVspP9Es0YMHlmdw6eobGm3xm14UqCKYkySygtXWPTfqPXonfDIMQeu0E0eil
Cg/TSSvm4CJB1u1JrpehzDsVUEOZPnBuO8axY2Rv5MBE3mMJVWzUN6oj5OOp06Gz
AgMBAAGjRTBDMA4GA1UdDwEB/wQEAwICpDASBgNVHRMBAf8ECDAGAQH/AgEAMB0G
A1UdDgQWBBTp+7an7Jz7tRjmpnBveqo1hpNIsTANBgkqhkiG9w0BAQsFAAOCAQEA
DIfuWUfB+lgrOE6qTpF5R+lbBu9oQr7XXLYFnOBjSdTr/V7tJr6GmBO5G9vVm57N
bAGQekVLDMtjvbbHtM3wmOW7O5g0wykMl/uHiHKbtfYEZ89CLxxdYOjQpzgzJHhF
QSfpvdFFG55+/9Gdb1yUJHZ5P54UgVGNtiX3Hnch/FwU5avPD6PAr5a4OrSp++/S
zB4Aw1vhO+4uage+j/TW6uF3YJQT/thVWG8z2vXJuej+i/HBjiviHMEQVdC13LaI
uy7oM4B/BrDebtUD+blCgOYZs24sWu2eiCtqKW5dtxveNcN/Hq1py4xPOqkESfic
29vnSmtl5vitgVMcMNKneQ==
-----END CERTIFICATE-----

0 comments on commit e15b8a0

Please sign in to comment.