Skip to content

Commit 88c2a75

Browse files
authored
[6.8] Add SAML AuthN request signing tests (#48444) (#61586)
- Add a unit test for our signing code - Change SAML IT to use signed authentication requests for Shibboleth to consume Backport of #48444
1 parent 37825c3 commit 88c2a75

File tree

14 files changed

+316
-36
lines changed

14 files changed

+316
-36
lines changed

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthnRequestBuilderTests.java

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
import org.opensaml.saml.saml2.core.AuthnRequest;
1717
import org.opensaml.saml.saml2.core.NameID;
1818
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
19-
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
20-
import org.opensaml.saml.saml2.metadata.SingleSignOnService;
2119

2220
import static org.hamcrest.Matchers.equalTo;
2321
import static org.hamcrest.Matchers.greaterThan;
@@ -38,17 +36,7 @@ public class SamlAuthnRequestBuilderTests extends SamlTestCase {
3836
@Before
3937
public void init() throws Exception {
4038
SamlUtils.initialize(logger);
41-
42-
final SingleSignOnService sso = SamlUtils.buildObject(SingleSignOnService.class, SingleSignOnService.DEFAULT_ELEMENT_NAME);
43-
sso.setLocation(IDP_URL);
44-
sso.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
45-
46-
final IDPSSODescriptor idpRole = SamlUtils.buildObject(IDPSSODescriptor.class, IDPSSODescriptor.DEFAULT_ELEMENT_NAME);
47-
idpRole.getSingleSignOnServices().add(sso);
48-
49-
idpDescriptor = SamlUtils.buildObject(EntityDescriptor.class, EntityDescriptor.DEFAULT_ELEMENT_NAME);
50-
idpDescriptor.setEntityID(IDP_ENTITY_ID);
51-
idpDescriptor.getRoleDescriptors().add(idpRole);
39+
idpDescriptor = buildIdPDescriptor(IDP_URL, IDP_ENTITY_ID);
5240
}
5341

5442
public void testBuildRequestWithPersistentNameAndNoForceAuth() throws Exception {

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRedirectTests.java

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77

88
import org.joda.time.DateTime;
99
import org.joda.time.DateTimeZone;
10+
import org.opensaml.saml.common.xml.SAMLConstants;
1011
import org.opensaml.saml.saml2.core.Issuer;
1112
import org.opensaml.saml.saml2.core.LogoutRequest;
1213
import org.opensaml.saml.saml2.core.NameID;
14+
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
1315
import org.opensaml.security.x509.X509Credential;
1416

1517
import java.io.UnsupportedEncodingException;
@@ -19,7 +21,9 @@
1921
import java.security.NoSuchAlgorithmException;
2022
import java.security.Signature;
2123
import java.security.SignatureException;
24+
import java.time.Clock;
2225
import java.util.Base64;
26+
import java.util.Collections;
2327

2428
import static java.util.Collections.emptySet;
2529
import static java.util.Collections.singleton;
@@ -29,6 +33,9 @@
2933
public class SamlRedirectTests extends SamlTestCase {
3034

3135
private static final String IDP_ENTITY_ID = "https://idp.test/";
36+
private static final String SP_ENTITY_ID = "https://sp.example.com/";
37+
private static final String ACS_URL = "https://sp.example.com/saml/acs";
38+
private static final String IDP_URL = "https://idp.test/saml/sso/redirect";
3239
private static final String LOGOUT_URL = "https://idp.test/saml/logout";
3340

3441
private static final SigningConfiguration NO_SIGNING = new SigningConfiguration(emptySet(), null);
@@ -87,33 +94,58 @@ public void testLogoutRequestSigning() throws Exception {
8794
final SamlRedirect redirect = new SamlRedirect(buildLogoutRequest(LOGOUT_URL + "?"), spConfig);
8895
final String url = redirect.getRedirectUrl();
8996
final String queryParam = url.split("\\?")[1].split("&Signature")[0];
90-
final String params[] = url.split("\\?")[1].split("&");
91-
assert (params.length == 3);
92-
String sigAlg = parseAndUrlDecodeParameter(params[1]);
93-
// We currently only support signing with SHA256withRSA, this test should be updated if we add support for more
94-
assertThat(sigAlg, equalTo("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"));
95-
sigAlg = "SHA256withRSA";
96-
final String signature = parseAndUrlDecodeParameter(params[2]);
97-
assertThat(validateSignature(queryParam, sigAlg, signature, credential), equalTo(true));
98-
assertThat(validateSignature(queryParam, sigAlg, signature, invalidCredential), equalTo(false));
99-
assertThat(validateSignature(queryParam.substring(0, queryParam.length() - 5), sigAlg, signature, credential), equalTo(false));
97+
final String signature = validateUrlAndGetSignature(redirect.getRedirectUrl());
98+
assertThat(validateSignature(queryParam, signature, credential), equalTo(true));
99+
assertThat(validateSignature(queryParam, signature, invalidCredential), equalTo(false));
100+
assertThat(validateSignature(queryParam.substring(0, queryParam.length() - 5), signature, credential), equalTo(false));
101+
}
102+
103+
public void testAuthnRequestSigning() throws Exception {
104+
final X509Credential credential = (X509Credential) buildOpenSamlCredential(readRandomKeyPair()).get(0);
105+
X509Credential invalidCredential = (X509Credential) buildOpenSamlCredential(readRandomKeyPair()).get(0);
106+
while (invalidCredential.getEntityCertificate().getSerialNumber().equals(credential.getEntityCertificate().getSerialNumber())) {
107+
invalidCredential = (X509Credential) buildOpenSamlCredential(readRandomKeyPair()).get(0);
108+
}
109+
final SigningConfiguration signingConfig = new SigningConfiguration(singleton("*"), credential);
110+
SpConfiguration sp = new SpConfiguration(SP_ENTITY_ID, ACS_URL, LOGOUT_URL, signingConfig, null, Collections.emptyList());
111+
112+
EntityDescriptor idpDescriptor = buildIdPDescriptor(IDP_URL, IDP_ENTITY_ID);
113+
114+
final SamlRedirect redirect = new SamlRedirect(new SamlAuthnRequestBuilder(sp, SAMLConstants.SAML2_POST_BINDING_URI,
115+
idpDescriptor, SAMLConstants.SAML2_REDIRECT_BINDING_URI, Clock.systemUTC()).build(), signingConfig);
116+
final String url = redirect.getRedirectUrl();
117+
final String queryParam = url.split("\\?")[1].split("&Signature")[0];
118+
final String signature = validateUrlAndGetSignature(redirect.getRedirectUrl());
119+
assertThat(validateSignature(queryParam, signature, credential), equalTo(true));
120+
assertThat(validateSignature(queryParam, signature, invalidCredential), equalTo(false));
121+
assertThat(validateSignature(queryParam.substring(0, queryParam.length() - 5), signature, credential),
122+
equalTo(false));
100123
}
101124

102125
private String parseAndUrlDecodeParameter(String parameter) throws UnsupportedEncodingException {
103126
final String value = parameter.split("=", 2)[1];
104127
return URLDecoder.decode(value, "UTF-8");
105128
}
106129

107-
private boolean validateSignature(String queryParam, String sigAlg, String signature, X509Credential credential) {
130+
private String validateUrlAndGetSignature(String url) throws UnsupportedEncodingException {
131+
final String params[] = url.split("\\?")[1].split("&");
132+
assert (params.length == 3);
133+
String sigAlg = parseAndUrlDecodeParameter(params[1]);
134+
// We currently only support signing with SHA256withRSA, this test should be updated if we add support for more
135+
assertThat(sigAlg, equalTo("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"));
136+
return parseAndUrlDecodeParameter(params[2]);
137+
}
138+
139+
private boolean validateSignature(String queryParam, String signature, X509Credential credential) {
108140
try {
109-
Signature sig = Signature.getInstance(sigAlg);
141+
// We currently only support signing with SHA256withRSA, this test should be updated if we add support for more
142+
Signature sig = Signature.getInstance("SHA256withRSA");
110143
sig.initVerify(credential.getPublicKey());
111144
sig.update(queryParam.getBytes(StandardCharsets.UTF_8));
112145
return sig.verify(Base64.getDecoder().decode(signature));
113146
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
114147
return false;
115148
}
116-
117149
}
118150

119151
private LogoutRequest buildLogoutRequest(String logoutUrl) {

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlTestCase.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
import org.elasticsearch.xpack.core.ssl.PemUtils;
1717
import org.junit.AfterClass;
1818
import org.junit.BeforeClass;
19+
import org.opensaml.saml.common.xml.SAMLConstants;
20+
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
21+
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
22+
import org.opensaml.saml.saml2.metadata.SingleSignOnService;
1923
import org.opensaml.security.credential.Credential;
2024
import org.opensaml.security.x509.impl.X509KeyManagerX509CredentialAdapter;
2125

@@ -135,4 +139,18 @@ protected ElasticsearchSecurityException expectSamlException(ThrowingRunnable ru
135139
assertThat("Exception " + exception + " should be a SAML exception", SamlUtils.isSamlException(exception), is(true));
136140
return exception;
137141
}
142+
143+
protected EntityDescriptor buildIdPDescriptor(String idpUrl, String idpEntityId) {
144+
final SingleSignOnService sso = SamlUtils.buildObject(SingleSignOnService.class, SingleSignOnService.DEFAULT_ELEMENT_NAME);
145+
sso.setLocation(idpUrl);
146+
sso.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
147+
148+
final IDPSSODescriptor idpRole = SamlUtils.buildObject(IDPSSODescriptor.class, IDPSSODescriptor.DEFAULT_ELEMENT_NAME);
149+
idpRole.getSingleSignOnServices().add(sso);
150+
151+
EntityDescriptor idpDescriptor = SamlUtils.buildObject(EntityDescriptor.class, EntityDescriptor.DEFAULT_ELEMENT_NAME);
152+
idpDescriptor.setEntityID(idpEntityId);
153+
idpDescriptor.getRoleDescriptors().add(idpRole);
154+
return idpDescriptor;
155+
}
138156
}

x-pack/qa/saml-idp-tests/build.gradle

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ testFixtures.useFixture ":x-pack:test:idp-fixture"
1717

1818
String outputDir = "${project.buildDir}/generated-resources/${project.name}"
1919
task copyIdpFiles(type: Copy) {
20-
from idpFixtureProject.files('idp/shibboleth-idp/credentials/idp-browser.pem', 'idp/shibboleth-idp/metadata/idp-metadata.xml');
20+
from idpFixtureProject.files('idp/shibboleth-idp/credentials/idp-browser.pem', 'idp/shibboleth-idp/metadata/idp-metadata.xml',
21+
'idp/shibboleth-idp/credentials/sp-signing.key', 'idp/shibboleth-idp/credentials/sp-signing.crt');
2122
into outputDir
2223
}
2324
project.sourceSets.test.output.dir(outputDir, builtBy: copyIdpFiles)
@@ -62,22 +63,39 @@ integTestCluster {
6263
setting 'xpack.security.authc.realms.shibboleth.sp.acs', 'http://localhost:54321/saml/acs1'
6364
setting 'xpack.security.authc.realms.shibboleth.attributes.principal', 'uid'
6465
setting 'xpack.security.authc.realms.shibboleth.attributes.name', 'urn:oid:2.5.4.3'
66+
setting 'xpack.security.authc.realms.shibboleth.signing.key', 'sp-signing.key'
67+
setting 'xpack.security.authc.realms.shibboleth.signing.certificate', 'sp-signing.crt'
6568
// SAML realm 2 (uses authorization_realms)
66-
setting 'xpack.security.authc.realms.shibboleth_native.type', 'saml'
6769
setting 'xpack.security.authc.realms.shibboleth_native.order', '2'
70+
setting 'xpack.security.authc.realms.shibboleth_native.type', 'saml'
6871
setting 'xpack.security.authc.realms.shibboleth_native.idp.entity_id', 'https://test.shibboleth.elastic.local/'
6972
setting 'xpack.security.authc.realms.shibboleth_native.idp.metadata.path', 'idp-metadata.xml'
7073
setting 'xpack.security.authc.realms.shibboleth_native.sp.entity_id', 'http://mock2.http.elastic.local/'
7174
setting 'xpack.security.authc.realms.shibboleth_native.sp.acs', 'http://localhost:54321/saml/acs2'
7275
setting 'xpack.security.authc.realms.shibboleth_native.attributes.principal', 'uid'
7376
setting 'xpack.security.authc.realms.shibboleth_native.authorization_realms', 'native'
77+
setting 'xpack.security.authc.realms.shibboleth_native.signing.key', 'sp-signing.key'
78+
setting 'xpack.security.authc.realms.shibboleth_native.signing.certificate', 'sp-signing.crt'
79+
// SAML realm 3 (used for negative tests with multiple realms)
80+
setting 'xpack.security.authc.realms.shibboleth_negative.order', '3'
81+
setting 'xpack.security.authc.realms.shibboleth_negative.type', 'saml'
82+
setting 'xpack.security.authc.realms.shibboleth_negative.idp.entity_id', 'https://test.shibboleth.elastic.local/'
83+
setting 'xpack.security.authc.realms.shibboleth_negative.idp.metadata.path', 'idp-metadata.xml'
84+
setting 'xpack.security.authc.realms.shibboleth_negative.sp.entity_id', 'somethingwronghere'
85+
setting 'xpack.security.authc.realms.shibboleth_negative.sp.acs', 'http://localhost:54321/saml/acs3'
86+
setting 'xpack.security.authc.realms.shibboleth_negative.attributes.principal', 'uid'
87+
setting 'xpack.security.authc.realms.shibboleth_negative.authorization_realms', 'native'
88+
setting 'xpack.security.authc.realms.shibboleth_negative.signing.key', 'sp-signing.key'
89+
setting 'xpack.security.authc.realms.shibboleth_negative.signing.certificate', 'sp-signing.crt'
90+
setting 'xpack.security.authc.realms.native.order', '4'
7491
setting 'xpack.security.authc.realms.native.type', 'native'
75-
setting 'xpack.security.authc.realms.native.order', '3'
7692

7793
setting 'xpack.ml.enabled', 'false'
7894
setting 'logger.org.elasticsearch.xpack.security', 'TRACE'
7995

8096
extraConfigFile 'idp-metadata.xml', file(outputDir + "/idp-metadata.xml")
97+
extraConfigFile 'sp-signing.key', file(outputDir + "/sp-signing.key")
98+
extraConfigFile 'sp-signing.crt', file(outputDir + "/sp-signing.crt")
8199

82100
setupCommand 'setupTestAdmin',
83101
'bin/elasticsearch-users', 'useradd', "test_admin", '-p', 'x-pack-test-password', '-r', "superuser"

x-pack/qa/saml-idp-tests/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticationIT.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,6 @@ public void setupNativeUser() throws IOException {
240240
* <li>Uses that token to verify the user details</li>
241241
* </ol>
242242
*/
243-
@AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/44410")
244243
public void testLoginUserWithSamlRoleMapping() throws Exception {
245244
// this ACS comes from the config in build.gradle
246245
final Tuple<String, String> authTokens = loginViaSaml("http://localhost:54321" + SP_ACS_PATH_1);
@@ -249,7 +248,6 @@ public void testLoginUserWithSamlRoleMapping() throws Exception {
249248
verifyElasticsearchAccessTokenForRoleMapping(accessToken);
250249
}
251250

252-
@AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/44410")
253251
public void testLoginUserWithAuthorizingRealm() throws Exception {
254252
// this ACS comes from the config in build.gradle
255253
final Tuple<String, String> authTokens = loginViaSaml("http://localhost:54321" + SP_ACS_PATH_2);

x-pack/test/idp-fixture/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,5 @@ services:
3737
volumes:
3838
- ./idp/shibboleth-idp/conf:/opt/shibboleth-idp/conf
3939
- ./idp/shibboleth-idp/credentials:/opt/shibboleth-idp/credentials
40+
- ./idp/shibboleth-idp/metadata:/opt/shibboleth-idp/metadata
4041
- ./idp/shib-jetty-base/start.d/ssl.ini:/opt/shib-jetty-base/start.d/ssl.ini

x-pack/test/idp-fixture/idp/shibboleth-idp/conf/idp.properties

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ idp.encryption.cert=%{idp.home}/credentials/idp-encryption.crt
5757
#idp.encryption.key.2 = %{idp.home}/credentials/idp-encryption-old.key
5858
#idp.encryption.cert.2 = %{idp.home}/credentials/idp-encryption-old.crt
5959

60+
6061
# Sets the bean ID to use as a default security configuration set
6162
#idp.security.config = shibboleth.DefaultSecurityConfiguration
6263

@@ -72,13 +73,13 @@ idp.encryption.cert=%{idp.home}/credentials/idp-encryption.crt
7273
#idp.trust.signatures = shibboleth.ChainingSignatureTrustEngine
7374
# To pick only one set to one of:
7475
# shibboleth.ExplicitKeySignatureTrustEngine, shibboleth.PKIXSignatureTrustEngine
75-
#idp.trust.certificates = shibboleth.ChainingX509TrustEngine
76+
#idp.trust.certificates = shibboleth.ExplicitKeyX509TrustEngine
7677
# To pick only one set to one of:
7778
# shibboleth.ExplicitKeyX509TrustEngine, shibboleth.PKIXX509TrustEngine
7879

7980
# If true, encryption will happen whenever a key to use can be located, but
8081
# failure to encrypt won't result in request failure.
81-
#idp.encryption.optional = false
82+
idp.encryption.optional = true
8283

8384
# Configuration of client- and server-side storage plugins
8485
#idp.storage.cleanupInterval = PT10M

x-pack/test/idp-fixture/idp/shibboleth-idp/conf/metadata-providers.xml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,11 @@
5959
responsibility to ensure that the contents on disk are trustworthy.
6060
-->
6161

62-
<!--
63-
<MetadataProvider id="LocalMetadata" xsi:type="FilesystemMetadataProvider" metadataFile="PATH_TO_YOUR_METADATA"/>
64-
-->
62+
<!-- One metadada file per realm that we use in our elasticsearch/x-pack/qa/saml-idp-tests/build.gradle -->
63+
<MetadataProvider id="LocalMetadata" xsi:type="FilesystemMetadataProvider" metadataFile="%{idp.home}/metadata/sp-metadata.xml"/>
64+
<MetadataProvider id="LocalMetadata2" xsi:type="FilesystemMetadataProvider" metadataFile="%{idp.home}/metadata/sp-metadata2.xml"/>
65+
<MetadataProvider id="LocalMetadata3" xsi:type="FilesystemMetadataProvider" metadataFile="%{idp.home}/metadata/sp-metadata3.xml"/>
66+
6567

6668

6769
<!--
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIFazCCA1OgAwIBAgIULAHFP2QmBv0Nwq85ggfJa3p1EjMwDQYJKoZIhvcNAQEL
3+
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
4+
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0xOTEwMTUxMzI0MzBaFw0xOTEx
5+
MTQxMzI0MzBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
6+
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB
7+
AQUAA4ICDwAwggIKAoICAQCeNmv5oV+QsORglGd62oSAklrevH8sbiMW6wym42c5
8+
E6Fctq/jLJrY1oLdwOUMynSkaPmh9fAcZHtLUsjx6x4QcMEq/ODqBGvoJNZwomt+
9+
uyVbpKDGjtrXZowGnUXPz7hsM2yofRiDUIjTsujrvFp/HqCRw/9K4Uw2x8EYL5FU
10+
vaxxd36mjwbsZCXIjkOsi+jW/bM9RWQd3KF38Ar0vIP/wYK/5s9U+KSuOP3KsSOU
11+
HSH/pnBFiXciU6bVHcZA3IRYnxzaTT2EQZEXyryfQuk3TWe9YvVusXs6kd9uW4x+
12+
Lx/wovx4zy2iUpXhO/MVB1IG1SNM4w5ZE94mrLDtG+yOkGXqReesodPZYBC4OOaG
13+
rZi4rH05FouU6p7QA+iqBCFNNLTrg0f62M/UcAVsANJqQUfFqe7pr1gewF6jlhBw
14+
HEds19XDPmUHredOjDeGJMeJaH3GS8/M6s+MQpckffdmvF4WHwGAJvRf4AXW+G3a
15+
9CTjq+E5I29+dWSvuMIIY/howKxQ9Q+IQhZYGhTH/Y9vpdfOdwVZYBB18zAI2VqJ
16+
18iOCpmd0n/E6ZPyugCSlVZzSjSP+i/xz3c9rJnLkZ5dsOVwloNlw9jaVc/InCUN
17+
KYyRhoxwkPRa9dzuogdDo41lz95xwikxDunTSUEU9yNjfofmWpuQMz5kYHWPsFbS
18+
PQIDAQABo1MwUTAdBgNVHQ4EFgQUgowa3NlK8Dtl5KW1vL44+m0LnMAwHwYDVR0j
19+
BBgwFoAUgowa3NlK8Dtl5KW1vL44+m0LnMAwDwYDVR0TAQH/BAUwAwEB/zANBgkq
20+
hkiG9w0BAQsFAAOCAgEAZLIRn3HnTkiP6wp0gSbKqWdThM47atKqeU5l0iBcyBte
21+
rNmkxnFD6z+6x6YkKtA1ttFTY4kzQ/lr0m+jRlWpMzNkBXU7bNvcbTA5LfrMq99l
22+
QJpPuJ8eFM/B2PbYm5o0SEiTKVyYyjNXHze/alkflUTtNYQMJtSKYjFPvhx8/mVx
23+
CHBC66K+oYydbuUgzR+WGrEhIkTOREeGmtVZ1M8zru8lbD1+I8T0eqcpBqPcRdmM
24+
zSJjJopr+fM0v9LIpi22c50yZtHCLj9iAv8bQRaYnm6aV3kCb1Qv/Wadqk/7oeie
25+
J16bv6VBSFLKyvez26HlYNNmsauz9cWN/h/LosURqmS6g+28bM+uH/UOp6CTgODp
26+
OYqRpKW6/rmVPVejx7RBp63nNa/NMhbEngsR3QsBT5qcaMv7NZbK8v/tZDik3z7T
27+
M4ld5hyUQw8WawopqhNAmTdu0ShBMQhWItQpZ3n12VNDFXXTbhdWxU/VY1hbesVu
28+
C9pRcZXpVgC+tgxjfiRrruETbQyRM73evfnaBCgIaWXcTcyBuHHKneL4dhyOuBkz
29+
4wgyJjVe88Cld/4VhPj2JqJasunfhgmknB6PmRdOQbFiIcYRIrdlk5ZblJzB+mnK
30+
BOe4cVwR3qN0Hi7WqK8loakPVNnnpu7kGUvBKOeiKrXCBMgslffMbTw2ViTtEJo=
31+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)