From 1e72fb4bc59db702731c0c1cc89b93a55dcc96d1 Mon Sep 17 00:00:00 2001 From: ytvnr Date: Thu, 5 Sep 2024 15:35:22 +0200 Subject: [PATCH] feat: implement mTLS policy https://gravitee.atlassian.net/browse/APIM-3985 --- pom.xml | 7 +- .../io/gravitee/policy/mtls/MtlsPolicy.java | 55 +++- .../mtls/MtlsPolicyIntegrationTest.java | 152 ++++++++-- .../gravitee/policy/mtls/MtlsPolicyTest.java | 279 ++++++++++++++++++ src/test/resources/ca.pem | 35 +++ src/test/resources/client.cer | 34 +++ src/test/resources/client.key | 52 ++++ 7 files changed, 583 insertions(+), 31 deletions(-) create mode 100644 src/test/java/io/gravitee/policy/mtls/MtlsPolicyTest.java create mode 100644 src/test/resources/ca.pem create mode 100644 src/test/resources/client.cer create mode 100644 src/test/resources/client.key diff --git a/pom.xml b/pom.xml index fa60016..085a579 100644 --- a/pom.xml +++ b/pom.xml @@ -36,7 +36,7 @@ 8.1.0 - 3.5.1 + 3.7.0 1.31.1 1.11.0 4.5.0-SNAPSHOT @@ -135,6 +135,11 @@ assertj-core test + + org.mockito + mockito-junit-jupiter + test + diff --git a/src/main/java/io/gravitee/policy/mtls/MtlsPolicy.java b/src/main/java/io/gravitee/policy/mtls/MtlsPolicy.java index 4782dd6..1976a45 100644 --- a/src/main/java/io/gravitee/policy/mtls/MtlsPolicy.java +++ b/src/main/java/io/gravitee/policy/mtls/MtlsPolicy.java @@ -31,13 +31,21 @@ * limitations under the License. */ +import io.gravitee.common.http.HttpStatusCode; +import io.gravitee.gateway.reactive.api.ExecutionFailure; import io.gravitee.gateway.reactive.api.context.HttpExecutionContext; import io.gravitee.gateway.reactive.api.policy.SecurityPolicy; import io.gravitee.gateway.reactive.api.policy.SecurityToken; import io.gravitee.policy.mtls.configuration.MtlsPolicyConfiguration; import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Maybe; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.DigestUtils; /** * @author Yann TAVERNIER (yann.tavernier at graviteesource.com) @@ -46,6 +54,10 @@ @Slf4j public class MtlsPolicy implements SecurityPolicy { + public static final String CLIENT_CERTIFICATE_MISSING = "CLIENT_CERTIFICATE_MISSING"; + public static final String CLIENT_CERTIFICATE_INVALID = "CLIENT_CERTIFICATE_INVALID"; + public static final String FAILURE_MESSAGE = "Unauthorized"; + public static final String SSL_SESSION_REQUIRED = "SSL_SESSION_REQUIRED"; private final MtlsPolicyConfiguration configuration; public MtlsPolicy(MtlsPolicyConfiguration configuration) { @@ -58,8 +70,30 @@ public String id() { } @Override - public Maybe extractSecurityToken(HttpExecutionContext httpExecutionContext) { - return Maybe.just(SecurityToken.none()); + public Maybe extractSecurityToken(HttpExecutionContext ctx) { + final SSLSession sslSession = ctx.request().sslSession(); + if (sslSession == null) { + return Maybe.empty(); + } + + final Certificate[] peerCertificates; + try { + peerCertificates = sslSession.getPeerCertificates(); + } catch (SSLPeerUnverifiedException e) { + return Maybe.empty(); + } + + if (peerCertificates == null || peerCertificates.length == 0) { + return Maybe.empty(); + } + + final String clientCertificate; + try { + clientCertificate = DigestUtils.md5DigestAsHex(peerCertificates[0].getEncoded()); + } catch (CertificateEncodingException e) { + return Maybe.empty(); + } + return Maybe.just(SecurityToken.forClientCertificate(clientCertificate)); } @Override @@ -79,6 +113,23 @@ public int order() { @Override public Completable onRequest(HttpExecutionContext ctx) { + final SSLSession sslSession = ctx.request().sslSession(); + if (sslSession == null) { + return interruptWith401(ctx, SSL_SESSION_REQUIRED); + } + final Certificate[] certificates; + try { + certificates = sslSession.getPeerCertificates(); + } catch (SSLPeerUnverifiedException e) { + return interruptWith401(ctx, CLIENT_CERTIFICATE_INVALID); + } + if (certificates == null || certificates.length == 0) { + return interruptWith401(ctx, CLIENT_CERTIFICATE_MISSING); + } return Completable.complete(); } + + private static Completable interruptWith401(HttpExecutionContext ctx, String errorKey) { + return ctx.interruptWith(new ExecutionFailure(HttpStatusCode.UNAUTHORIZED_401).key(errorKey).message(FAILURE_MESSAGE)); + } } diff --git a/src/test/java/io/gravitee/policy/mtls/MtlsPolicyIntegrationTest.java b/src/test/java/io/gravitee/policy/mtls/MtlsPolicyIntegrationTest.java index 5ed5fc5..40f1be7 100644 --- a/src/test/java/io/gravitee/policy/mtls/MtlsPolicyIntegrationTest.java +++ b/src/test/java/io/gravitee/policy/mtls/MtlsPolicyIntegrationTest.java @@ -24,57 +24,153 @@ import io.gravitee.apim.gateway.tests.sdk.AbstractPolicyTest; import io.gravitee.apim.gateway.tests.sdk.annotations.DeployApi; import io.gravitee.apim.gateway.tests.sdk.annotations.GatewayTest; +import io.gravitee.apim.gateway.tests.sdk.configuration.GatewayConfigurationBuilder; import io.gravitee.apim.gateway.tests.sdk.connector.EndpointBuilder; import io.gravitee.apim.gateway.tests.sdk.connector.EntrypointBuilder; import io.gravitee.common.http.HttpStatusCode; +import io.gravitee.node.api.certificate.KeyStoreLoader; import io.gravitee.plugin.endpoint.EndpointConnectorPlugin; import io.gravitee.plugin.endpoint.http.proxy.HttpProxyEndpointConnectorFactory; import io.gravitee.plugin.entrypoint.EntrypointConnectorPlugin; import io.gravitee.plugin.entrypoint.http.proxy.HttpProxyEntrypointConnectorFactory; import io.gravitee.policy.mtls.configuration.MtlsPolicyConfiguration; +import io.vertx.core.http.HttpClientOptions; import io.vertx.core.http.HttpMethod; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.rxjava3.core.Vertx; import io.vertx.rxjava3.core.buffer.Buffer; import io.vertx.rxjava3.core.http.HttpClient; import io.vertx.rxjava3.core.http.HttpClientRequest; +import java.net.URL; import java.util.Map; import java.util.concurrent.TimeUnit; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; /** * @author Yann TAVERNIER (yann.tavernier at graviteesource.com) * @author GraviteeSource Team */ -@GatewayTest -@DeployApi({ "/apis/v4/api.json" }) -public class MtlsPolicyIntegrationTest extends AbstractPolicyTest { +public class MtlsPolicyIntegrationTest { - @Override - public void configureEntrypoints(Map> entrypoints) { - entrypoints.putIfAbsent("http-proxy", EntrypointBuilder.build("http-proxy", HttpProxyEntrypointConnectorFactory.class)); - } + @Nested + @GatewayTest + @DeployApi({ "/apis/v4/api.json" }) + class SecuredGateway extends AbstractPolicyTest { + + @Override + public void configureEntrypoints(Map> entrypoints) { + entrypoints.putIfAbsent("http-proxy", EntrypointBuilder.build("http-proxy", HttpProxyEntrypointConnectorFactory.class)); + } + + @Override + public void configureEndpoints(Map> endpoints) { + endpoints.putIfAbsent("http-proxy", EndpointBuilder.build("http-proxy", HttpProxyEndpointConnectorFactory.class)); + } + + @SneakyThrows + @Override + protected void configureGateway(GatewayConfigurationBuilder config) { + config + .httpSecured(true) + .set("http.ssl.clientAuth", "request") + .set("http.ssl.keystore.type", KeyStoreLoader.CERTIFICATE_FORMAT_SELF_SIGNED) + .set("http.ssl.truststore.path", getUrl("ca.pem").getPath()) + .set("http.ssl.truststore.type", "pem") + .set("http.ssl.truststore.password", "secret"); + } + + @Test + protected void should_be_unauthorized_if_no_certificate_on_request(Vertx vertx) { + wiremock.stubFor(get("/endpoint").willReturn(ok("backend response"))); + + createTrustedHttpClient(vertx, false) + .rxRequest(HttpMethod.GET, "/test") + .flatMap(HttpClientRequest::rxSend) + .flatMap(response -> { + assertThat(response.statusCode()).isEqualTo(HttpStatusCode.UNAUTHORIZED_401); + return response.body(); + }) + .test() + .awaitDone(10, TimeUnit.SECONDS) + .assertComplete() + .assertValue(Buffer.buffer(MtlsPolicy.FAILURE_MESSAGE)) + .assertNoErrors(); + + wiremock.verify(0, getRequestedFor(urlPathEqualTo("/endpoint"))); + } + + @Test + protected void should_call_the_api_with_a_certificate_on_request(Vertx vertx) { + wiremock.stubFor(get("/endpoint").willReturn(ok("backend response"))); + + createTrustedHttpClient(vertx, true) + .rxRequest(HttpMethod.GET, "/test") + .flatMap(HttpClientRequest::rxSend) + .flatMap(response -> { + assertThat(response.statusCode()).isEqualTo(HttpStatusCode.OK_200); + return response.body(); + }) + .test() + .awaitDone(10, TimeUnit.SECONDS) + .assertComplete() + .assertValue(Buffer.buffer("backend response")) + .assertNoErrors(); - @Override - public void configureEndpoints(Map> endpoints) { - endpoints.putIfAbsent("http-proxy", EndpointBuilder.build("http-proxy", HttpProxyEndpointConnectorFactory.class)); + wiremock.verify(1, getRequestedFor(urlPathEqualTo("/endpoint"))); + } + + HttpClient createTrustedHttpClient(Vertx vertx, boolean withCert) { + var options = new HttpClientOptions().setSsl(true).setTrustAll(true).setDefaultPort(gatewayPort()).setDefaultHost("localhost"); + if (withCert) { + options = + options.setPemKeyCertOptions( + new PemKeyCertOptions().addCertPath(getUrl("client.cer").getPath()).addKeyPath(getUrl("client.key").getPath()) + ); + } + + return vertx.createHttpClient(options); + } + + URL getUrl(String name) { + return getClass().getClassLoader().getResource(name); + } } - @Test - protected void should_call_api_with_policy_on_request(HttpClient httpClient) { - wiremock.stubFor(get("/endpoint").willReturn(ok("backend response"))); - - httpClient - .rxRequest(HttpMethod.GET, "/test") - .flatMap(HttpClientRequest::rxSend) - .flatMap(response -> { - assertThat(response.statusCode()).isEqualTo(HttpStatusCode.OK_200); - return response.body(); - }) - .test() - .awaitDone(10, TimeUnit.SECONDS) - .assertComplete() - .assertValue(Buffer.buffer("backend response")) - .assertNoErrors(); - - wiremock.verify(1, getRequestedFor(urlPathEqualTo("/endpoint"))); + @Nested + @GatewayTest + @DeployApi({ "/apis/v4/api.json" }) + class UnsecuredGateway extends AbstractPolicyTest { + + @Override + public void configureEntrypoints(Map> entrypoints) { + entrypoints.putIfAbsent("http-proxy", EntrypointBuilder.build("http-proxy", HttpProxyEntrypointConnectorFactory.class)); + } + + @Override + public void configureEndpoints(Map> endpoints) { + endpoints.putIfAbsent("http-proxy", EndpointBuilder.build("http-proxy", HttpProxyEndpointConnectorFactory.class)); + } + + @Test + protected void should_be_unauthorized_if_no_certificate_on_request(HttpClient client) { + wiremock.stubFor(get("/endpoint").willReturn(ok("backend response"))); + + client + .rxRequest(HttpMethod.GET, "/test") + .flatMap(HttpClientRequest::rxSend) + .flatMap(response -> { + assertThat(response.statusCode()).isEqualTo(HttpStatusCode.UNAUTHORIZED_401); + return response.body(); + }) + .test() + .awaitDone(10, TimeUnit.SECONDS) + .assertComplete() + .assertValue(Buffer.buffer(MtlsPolicy.FAILURE_MESSAGE)) + .assertNoErrors(); + + wiremock.verify(0, getRequestedFor(urlPathEqualTo("/endpoint"))); + } } } diff --git a/src/test/java/io/gravitee/policy/mtls/MtlsPolicyTest.java b/src/test/java/io/gravitee/policy/mtls/MtlsPolicyTest.java new file mode 100644 index 0000000..982bd50 --- /dev/null +++ b/src/test/java/io/gravitee/policy/mtls/MtlsPolicyTest.java @@ -0,0 +1,279 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.policy.mtls; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.gravitee.common.http.HttpStatusCode; +import io.gravitee.gateway.reactive.api.policy.SecurityToken; +import io.gravitee.gateway.reactive.core.context.AbstractRequest; +import io.gravitee.gateway.reactive.core.context.AbstractResponse; +import io.gravitee.gateway.reactive.core.context.DefaultExecutionContext; +import io.gravitee.gateway.reactive.core.context.interruption.InterruptionFailureException; +import io.gravitee.policy.mtls.configuration.MtlsPolicyConfiguration; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.util.List; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import lombok.SneakyThrows; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * @author Yann TAVERNIER (yann.tavernier at graviteesource.com) + * @author GraviteeSource Team + */ +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@ExtendWith(MockitoExtension.class) +class MtlsPolicyTest { + + private final MtlsPolicy cut = new MtlsPolicy(new MtlsPolicyConfiguration()); + + @Nested + class TokenExtraction { + + @Test + void should_not_extract_token_when_no_ssl_session() { + final DefaultExecutionContext ctx = prepareContext(new AbstractRequest() {}); + + cut.extractSecurityToken(ctx).test().assertComplete().assertNoValues(); + } + + @Test + void should_not_extract_token_when_peer_certificate_exception() { + final DefaultExecutionContext ctx = prepareContext( + new AbstractRequest() { + @SneakyThrows + @Override + public SSLSession sslSession() { + final SSLSession sslSession = mock(SSLSession.class); + when(sslSession.getPeerCertificates()).thenThrow(SSLPeerUnverifiedException.class); + return sslSession; + } + } + ); + + cut.extractSecurityToken(ctx).test().assertComplete().assertNoValues(); + } + + @Test + void should_not_extract_token_when_peer_certificate_null_array() { + final DefaultExecutionContext ctx = prepareContext( + new AbstractRequest() { + @SneakyThrows + @Override + public SSLSession sslSession() { + final SSLSession sslSession = mock(SSLSession.class); + when(sslSession.getPeerCertificates()).thenReturn(null); + return sslSession; + } + } + ); + + cut.extractSecurityToken(ctx).test().assertComplete().assertNoValues(); + } + + @Test + void should_not_extract_token_when_peer_certificate_empty_array() { + final DefaultExecutionContext ctx = prepareContext( + new AbstractRequest() { + @SneakyThrows + @Override + public SSLSession sslSession() { + final SSLSession sslSession = mock(SSLSession.class); + when(sslSession.getPeerCertificates()).thenReturn(new Certificate[0]); + return sslSession; + } + } + ); + + cut.extractSecurityToken(ctx).test().assertComplete().assertNoValues(); + } + + @Test + void should_not_extract_token_when_digest_computation_exception() { + final DefaultExecutionContext ctx = prepareContext( + new AbstractRequest() { + @SneakyThrows + @Override + public SSLSession sslSession() { + final SSLSession sslSession = mock(SSLSession.class); + final Certificate certificate = mock(Certificate.class); + when(certificate.getEncoded()).thenThrow(CertificateEncodingException.class); + when(sslSession.getPeerCertificates()).thenReturn(List.of(certificate).toArray(new Certificate[0])); + return sslSession; + } + } + ); + + cut.extractSecurityToken(ctx).test().assertComplete().assertNoValues(); + } + + @Test + void should_extract_token() { + final DefaultExecutionContext ctx = prepareContext( + new AbstractRequest() { + @SneakyThrows + @Override + public SSLSession sslSession() { + final SSLSession sslSession = mock(SSLSession.class); + final Certificate certificate = mock(Certificate.class); + when(certificate.getEncoded()).thenReturn("a-certificate".getBytes()); + when(sslSession.getPeerCertificates()).thenReturn(List.of(certificate).toArray(new Certificate[0])); + return sslSession; + } + } + ); + + cut + .extractSecurityToken(ctx) + .test() + .assertComplete() + .assertValue(securityToken -> { + assertThat(securityToken.getTokenType()).isEqualTo(SecurityToken.TokenType.CERTIFICATE.name()); + return true; + }); + } + } + + @Nested + class OnRequest { + + @Test + void should_answer_with_401_when_no_ssl_session() { + final DefaultExecutionContext ctx = prepareContext(new AbstractRequest() {}); + + cut + .onRequest(ctx) + .test() + .assertError(t -> { + assertThat(t).isInstanceOf(InterruptionFailureException.class); + final InterruptionFailureException exception = (InterruptionFailureException) t; + assertThat(exception.getExecutionFailure().key()).isEqualTo(MtlsPolicy.SSL_SESSION_REQUIRED); + assertThat(exception.getExecutionFailure().statusCode()).isEqualTo(HttpStatusCode.UNAUTHORIZED_401); + return true; + }); + } + + @Test + void should_answer_with_401_when_peer_certificate_exception() { + final DefaultExecutionContext ctx = prepareContext( + new AbstractRequest() { + @SneakyThrows + @Override + public SSLSession sslSession() { + final SSLSession sslSession = mock(SSLSession.class); + when(sslSession.getPeerCertificates()).thenThrow(SSLPeerUnverifiedException.class); + return sslSession; + } + } + ); + + cut + .onRequest(ctx) + .test() + .assertError(t -> { + assertThat(t).isInstanceOf(InterruptionFailureException.class); + final InterruptionFailureException exception = (InterruptionFailureException) t; + assertThat(exception.getExecutionFailure().key()).isEqualTo(MtlsPolicy.CLIENT_CERTIFICATE_INVALID); + assertThat(exception.getExecutionFailure().statusCode()).isEqualTo(HttpStatusCode.UNAUTHORIZED_401); + return true; + }); + } + + @Test + void should_answer_with_401_when_peer_certificate_null_array() { + final DefaultExecutionContext ctx = prepareContext( + new AbstractRequest() { + @SneakyThrows + @Override + public SSLSession sslSession() { + final SSLSession sslSession = mock(SSLSession.class); + when(sslSession.getPeerCertificates()).thenReturn(null); + return sslSession; + } + } + ); + + cut + .onRequest(ctx) + .test() + .assertError(t -> { + assertThat(t).isInstanceOf(InterruptionFailureException.class); + final InterruptionFailureException exception = (InterruptionFailureException) t; + assertThat(exception.getExecutionFailure().key()).isEqualTo(MtlsPolicy.CLIENT_CERTIFICATE_MISSING); + assertThat(exception.getExecutionFailure().statusCode()).isEqualTo(HttpStatusCode.UNAUTHORIZED_401); + return true; + }); + } + + @Test + void should_answer_with_401_when_peer_certificate_empty_array() { + final DefaultExecutionContext ctx = prepareContext( + new AbstractRequest() { + @SneakyThrows + @Override + public SSLSession sslSession() { + final SSLSession sslSession = mock(SSLSession.class); + when(sslSession.getPeerCertificates()).thenReturn(new Certificate[0]); + return sslSession; + } + } + ); + + cut + .onRequest(ctx) + .test() + .assertError(t -> { + assertThat(t).isInstanceOf(InterruptionFailureException.class); + final InterruptionFailureException exception = (InterruptionFailureException) t; + assertThat(exception.getExecutionFailure().key()).isEqualTo(MtlsPolicy.CLIENT_CERTIFICATE_MISSING); + assertThat(exception.getExecutionFailure().statusCode()).isEqualTo(HttpStatusCode.UNAUTHORIZED_401); + return true; + }); + } + + @Test + void should_continue_request_if_certificate_exist() { + final DefaultExecutionContext ctx = prepareContext( + new AbstractRequest() { + @SneakyThrows + @Override + public SSLSession sslSession() { + final SSLSession sslSession = mock(SSLSession.class); + final Certificate certificate = mock(Certificate.class); + when(sslSession.getPeerCertificates()).thenReturn(List.of(certificate).toArray(new Certificate[0])); + return sslSession; + } + } + ); + + cut.onRequest(ctx).test().assertComplete(); + } + } + + private static DefaultExecutionContext prepareContext(AbstractRequest request) { + final DefaultExecutionContext ctx = new DefaultExecutionContext(request, new AbstractResponse() {}); + return ctx; + } +} diff --git a/src/test/resources/ca.pem b/src/test/resources/ca.pem new file mode 100644 index 0000000..144d2ec --- /dev/null +++ b/src/test/resources/ca.pem @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIGBTCCA+2gAwIBAgIULGameyQTEB6iQ0+WwG50XwjJLLMwDQYJKoZIhvcNAQEL +BQAwgZAxKTAnBgkqhkiG9w0BCQEWGmNvbnRhY3RAZ3Jhdml0ZWVzb3VyY2UuY29t +MRAwDgYDVQQDDAdBUElNX0NOMQ0wCwYDVQQLDARBUElNMRQwEgYDVQQKDAtBUElN +X1Rlc3RlcjEOMAwGA1UEBwwFTGlsbGUxDzANBgNVBAgMBkZyYW5jZTELMAkGA1UE +BhMCRlIwIBcNMjQwOTA1MTQzOTI1WhgPMjEyNDA4MTIxNDM5MjVaMIGQMSkwJwYJ +KoZIhvcNAQkBFhpjb250YWN0QGdyYXZpdGVlc291cmNlLmNvbTEQMA4GA1UEAwwH +QVBJTV9DTjENMAsGA1UECwwEQVBJTTEUMBIGA1UECgwLQVBJTV9UZXN0ZXIxDjAM +BgNVBAcMBUxpbGxlMQ8wDQYDVQQIDAZGcmFuY2UxCzAJBgNVBAYTAkZSMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0g5f78gn41pfs72TMzTXQ5qEUotq +5ujjHBvWWsxX27ikXStTDZpYQCID5PyDiS5iZKQ1R6fN65llQtE7+3ts+B75aGhm +o6s0Q04KDWiE5+Locj+b+UYqHuAlvwqYZcBqVRzoxerblhheoHWOS1nTeCU1ICy0 +I6qr8fteugyHNBvanZSAzM9mSJOPWjwTtcb+73q6eoBipdaRExeFO7tBKP+AQ0UP +5YQHBhELU6wuhY4sDRRQ343y4mCW2WuPKYv0BlQoQlkUP7cwcGDsGEaJ3dJ7xGai +PwyX4RdMfpxGGC0q64PQ+whgjolilNOxGKi3fQeXWIhwEpo41xgQk2B0Kp539Cdm +sK79/SG3ZSyrgLSYnHG88JL4rS4e+4OHMs53WephlXHeTuCvvPicAzNGcIj3J/G7 +/B5xihmtDNacT73V/AJ9wKOlmT9Uox2rKCjJJrH8vAwIS1GhCwmRcGpg7Bl4Oy67 +KK966mHrS+I5T3NaTC231PIfams24cHvuNBZ4xkVeJ+FOvs5I5nGbKM2APp/C6Rp +qoqsdwVXuWQM+rfc5Dw8Cvmxe5XgucWC0TF2wPDbf/OXjk3r66JOYs81+hTjHZ7y +WG5XE0QTCypRpuzrD7XFP6zm22rQROcysiBkimxm9H7kNBFP/ih3RXn72PjSbvcz +oupvvbokmtnMNpkCAwEAAaNTMFEwHQYDVR0OBBYEFLBqZKOHVxrkidG+Y2IDWkL3 +w1OnMB8GA1UdIwQYMBaAFLBqZKOHVxrkidG+Y2IDWkL3w1OnMA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQELBQADggIBAGMQ3gNfnC9mQEFBiMlIDGwjPpduyauY +HhI3xTyZ+GajHmMoEAJ/mQH9uWEY7O7H4pi2hNOd0citDinO+1qftSJABhMwcT6I +os7RN8ipuJj5XtgOXXSqw+ZnnWQ9m4NqsygHMqkyIcnnsNK2GwJH1bZ9pSro1rjC +C2ZxzZv8QUMgHakStPMpMgJJ4i5fWAytRGj6+ssn6wL1chHCnSla3c/JWGwFgajE +EiNqd0yT5dux47ds+EGzcDOSi13QwcxkwX2yQyo2zdmgguO7ZbT9VheiUBDTMQY8 +c5pf5Zbm8/hRYZ4YmUMUAmWby01+d/Hjul6gRJ+t5swh1oIhx3t5k97tHfAK26NO +SeyQm5ESh1M1JpKeC12GyCWCjyOeALiRKrkzKtptUqEr2hLGBRhfSMN4BTyOmKfp +zCK2vABrEaTDD4JvzF3OcbbtI+IDcP3mkmQxwrH94VVXqbJAVFCYk+RMfrC5GQi6 +5ytkhGHf1G4iPJb8QsAuDj9SYdfA8aeL/wGBwJ69dPOcns3CTmIm/nMy041tScdf +/ksPaQXi9nGEo6fGUsi0IFHor70uFbAPeyjLUOcPRazwgAUHC7eqksYWZRiwME+3 +uepI/7yoUJeqfVYq9c0IRyc8ne2M5SEk5H3mBAZ0ebAIeusiMInEvs/vVHEG/0TN +MuO0NIgZlXpW +-----END CERTIFICATE----- diff --git a/src/test/resources/client.cer b/src/test/resources/client.cer new file mode 100644 index 0000000..8b0569c --- /dev/null +++ b/src/test/resources/client.cer @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +k6Ft1v0NArfxRyNRDGr0t/zWpg/7 +-----END CERTIFICATE----- diff --git a/src/test/resources/client.key b/src/test/resources/client.key new file mode 100644 index 0000000..af01441 --- /dev/null +++ b/src/test/resources/client.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +B0nrhnBgSfXS39A2MWpRY50HFRGhFC9a +-----END PRIVATE KEY-----