From fc7d27d5228d81381a60c9ad872e55e4ec770a25 Mon Sep 17 00:00:00 2001 From: Guillaume Lamirand Date: Wed, 30 Aug 2023 15:32:05 +0200 Subject: [PATCH] feat: add new option allowing to check confirmation method - the option is enabled on the oauth2 policy and only support bound certificate (x5t#S256) --- README.adoc | 42 +- pom.xml | 4 +- .../gravitee/policy/oauth2/Oauth2Policy.java | 18 + .../OAuth2PolicyConfiguration.java | 74 +-- .../TokenIntrospectionResult.java | 15 +- .../policy/v3/oauth2/Oauth2PolicyV3.java | 52 ++ src/main/resources/schemas/schema-form.json | 37 ++ .../policy/oauth2/DummyOAuth2Resource.java | 7 + ...cyV4EmulationEngineCnfIntegrationTest.java | 476 ++++++++++++++++++ .../Oauth2PolicyV3CnfIntegrationTest.java | 90 ++++ src/test/resources/certificate/ca-crt.pem | 17 + src/test/resources/certificate/ca-crt.srl | 1 + src/test/resources/certificate/ca-key.pem | 30 ++ .../resources/certificate/client1-crt.pem | 22 + .../resources/certificate/client1-csr.pem | 26 + .../resources/certificate/client1-key.pem | 51 ++ src/test/resources/certificate/keystore.p12 | Bin 0 -> 2285 bytes 17 files changed, 907 insertions(+), 55 deletions(-) create mode 100644 src/test/java/io/gravitee/policy/oauth2/Oauth2PolicyV4EmulationEngineCnfIntegrationTest.java create mode 100644 src/test/java/io/gravitee/policy/v3/oauth2/Oauth2PolicyV3CnfIntegrationTest.java create mode 100644 src/test/resources/certificate/ca-crt.pem create mode 100644 src/test/resources/certificate/ca-crt.srl create mode 100644 src/test/resources/certificate/ca-key.pem create mode 100644 src/test/resources/certificate/client1-crt.pem create mode 100644 src/test/resources/certificate/client1-csr.pem create mode 100644 src/test/resources/certificate/client1-key.pem create mode 100644 src/test/resources/certificate/keystore.p12 diff --git a/README.adoc b/README.adoc index 9a3046d2..48fc3c0e 100644 --- a/README.adoc +++ b/README.adoc @@ -121,6 +121,36 @@ APIM supports two types of authorization server: ^.^|array of string |=== +=== Specific configuration for Confirmation Method validation + +|=== +|Property |Required |Description |Type| Default + +.^|confirmationMethodValidation.ignoreMissing +^.^|- +|Will ignore CNF validation if the token doesn't contain any CNF information. +^.^|boolean +^.^|false + +.^|confirmationMethodValidation.certificateBoundThumbprint.enabled +^.^|- +|Will validate the certificate thumbprint extracted from the access_token with the one provided by the client. +^.^|boolean +^.^|false + +.^|confirmationMethodValidation.certificateBoundThumbprint.extractCertificateFromHeader +^.^|- +|Enabled to extract the client certificate from request header. Necessary when the M-TLS connection is handled by a proxy. +^.^|boolean +^.^|false + +.^|confirmationMethodValidation.certificateBoundThumbprint.headerName +^.^|- +|Name of the header where to find the client certificate. +^.^|string +^.^|ssl-client-cert +|=== + === Configuration example [source, json] @@ -131,7 +161,15 @@ APIM supports two types of authorization server: "oauthCacheResource": "cache-resource-name", "extractPayload": true, "checkRequiredScopes": true, - "requiredScopes": ["openid", "resource:read", "resource:write"] + "requiredScopes": ["openid", "resource:read", "resource:write"], + "confirmationMethodValidation" : { + "ignoreMissing": false, + "certificateBoundThumbprint" : { + "enabled": false, + "extractCertificateFromHeader": false, + "headerName": "ssl-client-cert" + } + } } } ---- @@ -154,6 +192,8 @@ APIM supports two types of authorization server: * Access token can not be validated by authorization server +* Confirmation method can not be validated + .^| ```403``` | Issue encountered: diff --git a/pom.xml b/pom.xml index 4c494764..59994673 100644 --- a/pom.xml +++ b/pom.xml @@ -38,8 +38,8 @@ 3.0.0 1.11.0 4.0.0 - 2.1.1 - 4.0.0-SNAPSHOT + 3.0.0 + 4.1.0-SNAPSHOT 1.1.0 1.3.0 1.4.0 diff --git a/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java b/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java index 6b15bd9f..87f7a680 100644 --- a/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java +++ b/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java @@ -196,6 +196,24 @@ private Completable validateOAuth2Payload( } } + // Check confirmation Method and certificate thumbprint + OAuth2PolicyConfiguration.ConfirmationMethodValidation confirmationMethodValidation = + oAuth2PolicyConfiguration.getConfirmationMethodValidation(); + if (confirmationMethodValidation != null && confirmationMethodValidation.getCertificateBoundThumbprint().isEnabled()) { + String tokenThumbprint = tokenIntrospectionResult.extractPath(OAUTH_PAYLOAD_CNF).path(OAUTH_PAYLOAD_X5T).asText(); + if ( + !isValidCertificateThumbprint( + tokenThumbprint, + ctx.request().sslSession(), + ctx.request().headers(), + confirmationMethodValidation.isIgnoreMissing(), + confirmationMethodValidation.getCertificateBoundThumbprint() + ) + ) { + return sendError(ctx, OAUTH2_INVALID_CERTIFICATE_BOUND_THUMBPRINT); + } + } + // Store OAuth2 payload into execution context if required if (oAuth2PolicyConfiguration.isExtractPayload()) { ctx.setAttribute(CONTEXT_ATTRIBUTE_OAUTH_PAYLOAD, tokenIntrospectionResult.getOauth2ResponsePayload()); diff --git a/src/main/java/io/gravitee/policy/oauth2/configuration/OAuth2PolicyConfiguration.java b/src/main/java/io/gravitee/policy/oauth2/configuration/OAuth2PolicyConfiguration.java index 41af9b14..57e17ff6 100644 --- a/src/main/java/io/gravitee/policy/oauth2/configuration/OAuth2PolicyConfiguration.java +++ b/src/main/java/io/gravitee/policy/oauth2/configuration/OAuth2PolicyConfiguration.java @@ -18,11 +18,19 @@ import io.gravitee.policy.api.PolicyConfiguration; import java.util.ArrayList; import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; /** * @author David BRASSELY (david.brassely at graviteesource.com) * @author GraviteeSource Team */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter public class OAuth2PolicyConfiguration implements PolicyConfiguration { private String oauthResource; @@ -31,61 +39,29 @@ public class OAuth2PolicyConfiguration implements PolicyConfiguration { private boolean checkRequiredScopes = false; private List requiredScopes = new ArrayList<>(); private boolean modeStrict = true; - private boolean propagateAuthHeader = true; - - public String getOauthResource() { - return oauthResource; - } - - public void setOauthResource(String oauthResource) { - this.oauthResource = oauthResource; - } - - public boolean isExtractPayload() { - return extractPayload; - } - - public void setExtractPayload(boolean extractPayload) { - this.extractPayload = extractPayload; - } - - public boolean isCheckRequiredScopes() { - return checkRequiredScopes; - } - public void setCheckRequiredScopes(boolean checkRequiredScopes) { - this.checkRequiredScopes = checkRequiredScopes; - } - - public List getRequiredScopes() { - return requiredScopes; - } + private ConfirmationMethodValidation confirmationMethodValidation = new ConfirmationMethodValidation(); - public void setRequiredScopes(List requiredScopes) { - this.requiredScopes = requiredScopes; - } - - public boolean isModeStrict() { - return modeStrict; - } + private boolean propagateAuthHeader = true; - public void setModeStrict(boolean modeStrict) { - this.modeStrict = modeStrict; - } + @NoArgsConstructor + @AllArgsConstructor + @Getter + @Setter + public static class ConfirmationMethodValidation { - public boolean isPropagateAuthHeader() { - return propagateAuthHeader; + private boolean ignoreMissing = false; + private CertificateBoundThumbprint certificateBoundThumbprint = new CertificateBoundThumbprint(); } - public void setPropagateAuthHeader(boolean propagateAuthHeader) { - this.propagateAuthHeader = propagateAuthHeader; - } - - public String getOauthCacheResource() { - return oauthCacheResource; - } + @NoArgsConstructor + @AllArgsConstructor + @Getter + @Setter + public static class CertificateBoundThumbprint { - public void setOauthCacheResource(String oauthCacheResource) { - this.oauthCacheResource = oauthCacheResource; + private boolean enabled = false; + private boolean extractCertificateFromHeader = false; + private String headerName = "ssl-client-cert"; } } diff --git a/src/main/java/io/gravitee/policy/oauth2/introspection/TokenIntrospectionResult.java b/src/main/java/io/gravitee/policy/oauth2/introspection/TokenIntrospectionResult.java index e79c2af5..480b3053 100644 --- a/src/main/java/io/gravitee/policy/oauth2/introspection/TokenIntrospectionResult.java +++ b/src/main/java/io/gravitee/policy/oauth2/introspection/TokenIntrospectionResult.java @@ -60,8 +60,9 @@ public TokenIntrospectionResult(String oauth2ResponsePayload) { } public String getClientId() { - if (hasValidPayload()) { - return oAuth2ResponseJsonNode.path(OAUTH_PAYLOAD_CLIENT_ID_NODE).asText(); + JsonNode extractPath = extractPath(OAUTH_PAYLOAD_CLIENT_ID_NODE); + if (extractPath != null) { + return extractPath.asText(); } return null; } @@ -101,8 +102,16 @@ public List extractScopes(String scopeSeparator) { } public String extractUser(String userClaim) { + JsonNode extractPath = extractPath(userClaim == null ? OAUTH_PAYLOAD_SUB_NODE : userClaim); + if (extractPath != null) { + return extractPath.asText(); + } + return null; + } + + public JsonNode extractPath(String path) { if (hasValidPayload()) { - return oAuth2ResponseJsonNode.path(userClaim == null ? OAUTH_PAYLOAD_SUB_NODE : userClaim).asText(); + return oAuth2ResponseJsonNode.path(path); } return null; } diff --git a/src/main/java/io/gravitee/policy/v3/oauth2/Oauth2PolicyV3.java b/src/main/java/io/gravitee/policy/v3/oauth2/Oauth2PolicyV3.java index 89a63e51..4578207d 100644 --- a/src/main/java/io/gravitee/policy/v3/oauth2/Oauth2PolicyV3.java +++ b/src/main/java/io/gravitee/policy/v3/oauth2/Oauth2PolicyV3.java @@ -22,11 +22,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import io.gravitee.common.http.HttpStatusCode; +import io.gravitee.common.security.CertificateUtils; import io.gravitee.gateway.api.ExecutionContext; import io.gravitee.gateway.api.Request; import io.gravitee.gateway.api.Response; import io.gravitee.gateway.api.handler.Handler; import io.gravitee.gateway.api.http.HttpHeaderNames; +import io.gravitee.gateway.api.http.HttpHeaders; import io.gravitee.policy.api.PolicyChain; import io.gravitee.policy.api.PolicyResult; import io.gravitee.policy.api.annotations.OnRequest; @@ -39,11 +41,14 @@ import io.gravitee.resource.oauth2.api.OAuth2Resource; import io.gravitee.resource.oauth2.api.OAuth2Response; import java.io.IOException; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.Optional; +import javax.net.ssl.SSLSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; @@ -64,6 +69,8 @@ public class Oauth2PolicyV3 { public static final String OAUTH_PAYLOAD_CLIENT_ID_NODE = "client_id"; public static final String OAUTH_PAYLOAD_SUB_NODE = "sub"; public static final String OAUTH_PAYLOAD_EXP = "exp"; + public static final String OAUTH_PAYLOAD_CNF = "cnf"; + public static final String OAUTH_PAYLOAD_X5T = "x5t#S256"; public static final String CONTEXT_ATTRIBUTE_PREFIX = "oauth."; public static final String CONTEXT_ATTRIBUTE_OAUTH_PAYLOAD = CONTEXT_ATTRIBUTE_PREFIX + "payload"; @@ -76,6 +83,7 @@ public class Oauth2PolicyV3 { public static final String OAUTH2_INVALID_ACCESS_TOKEN_KEY = "OAUTH2_INVALID_ACCESS_TOKEN"; public static final String OAUTH2_INVALID_SERVER_RESPONSE_KEY = "OAUTH2_INVALID_SERVER_RESPONSE"; public static final String OAUTH2_INSUFFICIENT_SCOPE_KEY = "OAUTH2_INSUFFICIENT_SCOPE"; + public static final String OAUTH2_INVALID_CERTIFICATE_BOUND_THUMBPRINT = "OAUTH2_INVALID_CERTIFICATE_BOUND_THUMBPRINT"; public static final String OAUTH2_SERVER_UNAVAILABLE_KEY = "OAUTH2_SERVER_UNAVAILABLE"; public static final String OAUTH2_UNAUTHORIZED_MESSAGE = "Unauthorized"; @@ -224,6 +232,25 @@ private void handleSuccess( } } + // Check confirmation Method and certificate thumbprint + OAuth2PolicyConfiguration.ConfirmationMethodValidation confirmationMethodValidation = + oAuth2PolicyConfiguration.getConfirmationMethodValidation(); + if (confirmationMethodValidation != null && confirmationMethodValidation.getCertificateBoundThumbprint().isEnabled()) { + String tokenThumbprint = oauthResponseNode.path(OAUTH_PAYLOAD_CNF).path(OAUTH_PAYLOAD_X5T).asText(); + if ( + !isValidCertificateThumbprint( + tokenThumbprint, + request.sslSession(), + request.headers(), + confirmationMethodValidation.isIgnoreMissing(), + confirmationMethodValidation.getCertificateBoundThumbprint() + ) + ) { + sendError(OAUTH2_INVALID_CERTIFICATE_BOUND_THUMBPRINT, response, policyChain); + return; + } + } + // Store OAuth2 payload into execution context if required if (oAuth2PolicyConfiguration.isExtractPayload()) { executionContext.setAttribute(CONTEXT_ATTRIBUTE_OAUTH_PAYLOAD, oauth2payload); @@ -301,4 +328,29 @@ protected static boolean hasRequiredScopes(Collection tokenScopes, List< return tokenScopes.stream().anyMatch(requiredScopes::contains); } } + + protected static boolean isValidCertificateThumbprint( + final String tokenThumbprint, + final SSLSession sslSession, + final HttpHeaders headers, + final boolean ignoreMissingCnf, + final OAuth2PolicyConfiguration.CertificateBoundThumbprint certificateBoundThumbprint + ) { + // Ignore empty configuration method + if (!StringUtils.hasText(tokenThumbprint) && ignoreMissingCnf) { + return true; + } + + // Compute client certificate thumbprint + Optional clientCertificate; + if (certificateBoundThumbprint.isExtractCertificateFromHeader()) { + clientCertificate = CertificateUtils.extractCertificate(headers, certificateBoundThumbprint.getHeaderName()); + } else { + clientCertificate = CertificateUtils.extractPeerCertificate(sslSession); + } + return clientCertificate + .map(x509Certificate -> CertificateUtils.generateThumbprint(x509Certificate, "SHA-256")) + .map(tokenThumbprint::equals) + .orElse(false); + } } diff --git a/src/main/resources/schemas/schema-form.json b/src/main/resources/schemas/schema-form.json index 793f9e79..b53ca437 100644 --- a/src/main/resources/schemas/schema-form.json +++ b/src/main/resources/schemas/schema-form.json @@ -65,6 +65,43 @@ "type": "boolean", "default": true }, + "confirmationMethodValidation": { + "type": "object", + "title": "Confirmation Method Validation", + "properties": { + "ignoreMissing": { + "title": "Ignore missing CNF", + "description": "Will ignore CNF validation if the token doesn't contain any CNF information. Default is false.", + "type": "boolean", + "default": false + }, + "certificateBoundThumbprint": { + "type": "object", + "title": "Certificate Bound thumbprint (x5t#S256)", + "properties": { + "enabled": { + "title": "Enable certificate bound thumbprint validation", + "description": "Will validate the certificate thumbprint extracted from the access_token with the one provided by the client. The default is false.", + "type": "boolean", + "default": false + }, + "extractCertificateFromHeader": { + "title": "Extract client certificate from headers", + "description": "Enabled to extract the client certificate from request header. Necessary when the M-TLS connection is handled by a proxy.", + "type": "boolean", + "default": false + }, + "headerName": { + "title": "Header name", + "description": "Name of the header where to find the client certificate.", + "type": "string", + "default": "ssl-client-cert" + } + } + } + }, + "additionalProperties": false + }, "propagateAuthHeader": { "title": "Permit authorization header to the target endpoints", "description": "Allows to propagate Authorization header to the target endpoints", diff --git a/src/test/java/io/gravitee/policy/oauth2/DummyOAuth2Resource.java b/src/test/java/io/gravitee/policy/oauth2/DummyOAuth2Resource.java index 3feb4a89..c12f964b 100644 --- a/src/test/java/io/gravitee/policy/oauth2/DummyOAuth2Resource.java +++ b/src/test/java/io/gravitee/policy/oauth2/DummyOAuth2Resource.java @@ -30,6 +30,7 @@ public class DummyOAuth2Resource extends OAuth2Resource responseHandl response = new OAuth2Response(true, "{}"); } else if (TOKEN_SUCCESS_WITH_INVALID_PAYLOAD.equals(accessToken)) { response = new OAuth2Response(true, "{this _is _invalid json"); + } else if (TOKEN_SUCCESS_WITH_CNF.equals(accessToken)) { + response = + new OAuth2Response( + true, + "{\"client_id\": \"" + CLIENT_ID + "\", \"cnf\": { \"x5t#S256\" : \"2oHrNOqScxD8EHkb7_GYmnNvWqGj5M31Dqsrk3Jl2Yk\"}}" + ); } else if (TOKEN_FAIL.equals(accessToken)) { response = new OAuth2Response(false, null); } else { diff --git a/src/test/java/io/gravitee/policy/oauth2/Oauth2PolicyV4EmulationEngineCnfIntegrationTest.java b/src/test/java/io/gravitee/policy/oauth2/Oauth2PolicyV4EmulationEngineCnfIntegrationTest.java new file mode 100644 index 00000000..f5e4bb51 --- /dev/null +++ b/src/test/java/io/gravitee/policy/oauth2/Oauth2PolicyV4EmulationEngineCnfIntegrationTest.java @@ -0,0 +1,476 @@ +/* + * 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.oauth2; + +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.ok; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static io.gravitee.policy.oauth2.DummyOAuth2Resource.CLIENT_ID; +import static java.util.concurrent.TimeUnit.HOURS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.WireMockServer; +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.resource.ResourceBuilder; +import io.gravitee.definition.model.Api; +import io.gravitee.definition.model.Plan; +import io.gravitee.gateway.api.service.Subscription; +import io.gravitee.gateway.api.service.SubscriptionService; +import io.gravitee.gateway.reactive.api.policy.SecurityToken; +import io.gravitee.plugin.resource.ResourcePlugin; +import io.gravitee.policy.oauth2.configuration.OAuth2PolicyConfiguration; +import io.gravitee.policy.v3.oauth2.Oauth2PolicyV3IntegrationTest; +import io.reactivex.rxjava3.core.Single; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.rxjava3.core.http.HttpClient; +import io.vertx.rxjava3.core.http.HttpClientResponse; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.Collections; +import java.util.Date; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.stubbing.OngoingStubbing; + +/** + * @author GraviteeSource Team + */ +public class Oauth2PolicyV4EmulationEngineCnfIntegrationTest { + + public static final String API_ID = "my-api"; + public static final String PLAN_ID = "plan-id"; + + public static void configureHttpClient(HttpClientOptions options, int gatewayPort) { + options.setDefaultHost("localhost").setDefaultPort(gatewayPort).setSsl(true).setVerifyHost(false).setTrustAll(true); + } + + public static void configureGateway(GatewayConfigurationBuilder gatewayConfigurationBuilder) { + gatewayConfigurationBuilder + .set("http.secured", true) + .set("http.alpn", true) + .set("http.ssl.keystore.type", "self-signed") + .set("http.ssl.clientAuth", "request") + .set("http.ssl.truststore.type", "pkcs12") + .set("http.ssl.truststore.path", Oauth2PolicyV3IntegrationTest.class.getResource("/certificate/keystore.p12").getPath()) + .set("http.ssl.truststore.password", "gravitee"); + } + + public static void configureResources(Map resources) { + resources.put("dummy-oauth2-resource", ResourceBuilder.build("dummy-oauth2-resource", DummyOAuth2Resource.class)); + } + + public static Subscription fakeSubscriptionFromCache(boolean isExpired) { + final Subscription subscription = new Subscription(); + subscription.setApplication("application-id"); + subscription.setId("subscription-id"); + subscription.setPlan(PLAN_ID); + if (isExpired) { + subscription.setEndingAt(new Date(Instant.now().minus(1, HOURS.toChronoUnit()).toEpochMilli())); + } + return subscription; + } + + public static SecurityToken securityTokenMatcher(String clientId) { + return argThat(securityToken -> + securityToken.getTokenType().equals(SecurityToken.TokenType.CLIENT_ID.name()) && securityToken.getTokenValue().equals(clientId) + ); + } + + public static Plan createOauth2Plan(final Api api) { + Plan oauth2Plan = new Plan(); + oauth2Plan.setId(PLAN_ID); + oauth2Plan.setApi(api.getId()); + oauth2Plan.setSecurity("OAUTH2"); + oauth2Plan.setStatus("PUBLISHED"); + return oauth2Plan; + } + + public static void assert401unauthorized(WireMockServer wiremock, Single httpClientResponse) + throws InterruptedException { + httpClientResponse + .flatMapPublisher(response -> { + assertThat(response.statusCode()).isEqualTo(401); + return response.body().toFlowable(); + }) + .test() + .await() + .assertComplete() + .assertValue(body -> { + assertThat(body.toString()).isEqualTo("Unauthorized"); + return true; + }) + .assertNoErrors(); + + wiremock.verify(0, getRequestedFor(urlPathEqualTo("/team"))); + } + + @Nested + @GatewayTest + @DeployApi("/apis/oauth2.json") + public class Oauth2PolicyV4EmulationEngineMissingCnfIntegrationTest extends AbstractOauth2PolicyMissingCnfIntegrationTest {} + + public static class AbstractOauth2PolicyMissingCnfIntegrationTest extends AbstractPolicyTest { + + @Override + public void configureGateway(GatewayConfigurationBuilder gatewayConfigurationBuilder) { + Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureGateway(gatewayConfigurationBuilder); + } + + @Override + protected void configureHttpClient(final HttpClientOptions options) { + Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureHttpClient(options, gatewayPort()); + } + + @Override + public void configureApi(final Api api) { + Plan oauth2Plan = createOauth2Plan(api); + + OAuth2PolicyConfiguration configuration = new OAuth2PolicyConfiguration(); + configuration.setOauthResource("dummy-oauth2-resource"); + configuration.getConfirmationMethodValidation().setIgnoreMissing(true); + configuration.getConfirmationMethodValidation().getCertificateBoundThumbprint().setEnabled(true); + + try { + oauth2Plan.setSecurityDefinition(new ObjectMapper().writeValueAsString(configuration)); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to set OAuth2 policy configuration", e); + } + + api.setPlans(Collections.singletonList(oauth2Plan)); + } + + @Override + public void configureResources(final Map resources) { + Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureResources(resources); + } + + @Test + void should_access_api_and_ignore_missing_cnf(HttpClient client) throws InterruptedException { + wiremock.stubFor(get("/team").willReturn(ok("response from backend"))); + + // subscription found is valid + whenSearchingSubscription(API_ID, CLIENT_ID, PLAN_ID).thenReturn(Optional.of(fakeSubscriptionFromCache(false))); + + client + .rxRequest(HttpMethod.GET, "/test") + .flatMap(request -> + request.putHeader("Authorization", "Bearer " + DummyOAuth2Resource.TOKEN_SUCCESS_WITH_CLIENT_ID).rxSend() + ) + .flatMapPublisher(response -> { + assertThat(response.statusCode()).isEqualTo(200); + return response.toFlowable(); + }) + .test() + .await() + .assertComplete() + .assertValue(body -> { + assertThat(body).hasToString("response from backend"); + return true; + }) + .assertNoErrors(); + + wiremock.verify(1, getRequestedFor(urlPathEqualTo("/team"))); + } + + protected OngoingStubbing> whenSearchingSubscription(String api, String clientId, String plan) { + return when(getBean(SubscriptionService.class).getByApiAndSecurityToken(eq(api), securityTokenMatcher(clientId), eq(plan))); + } + } + + @Nested + @DeployApi("/apis/oauth2.json") + @GatewayTest + public class Oauth2PolicyV4EmulationEngineCnfHeaderCertificateIntegrationTest + extends AbstractOauth2PolicyCnfHeaderCertificateIntegrationTest {} + + public static class AbstractOauth2PolicyCnfHeaderCertificateIntegrationTest + extends AbstractPolicyTest { + + @Override + public void configureGateway(GatewayConfigurationBuilder gatewayConfigurationBuilder) { + Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureGateway(gatewayConfigurationBuilder); + } + + @Override + protected void configureHttpClient(final HttpClientOptions options) { + Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureHttpClient(options, gatewayPort()); + } + + @Override + public void configureApi(final Api api) { + Plan oauth2Plan = createOauth2Plan(api); + + OAuth2PolicyConfiguration configuration = new OAuth2PolicyConfiguration(); + configuration.setOauthResource("dummy-oauth2-resource"); + configuration.getConfirmationMethodValidation().getCertificateBoundThumbprint().setEnabled(true); + configuration.getConfirmationMethodValidation().getCertificateBoundThumbprint().setExtractCertificateFromHeader(true); + try { + oauth2Plan.setSecurityDefinition(new ObjectMapper().writeValueAsString(configuration)); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to set OAuth2 policy configuration", e); + } + + api.setPlans(Collections.singletonList(oauth2Plan)); + } + + @Override + public void configureResources(final Map resources) { + Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureResources(resources); + } + + @Test + void should_access_api_with_valid_certificate_from_header(HttpClient client) + throws InterruptedException, URISyntaxException, IOException { + wiremock.stubFor(get("/team").willReturn(ok("response from backend"))); + + // subscription found is valid + whenSearchingSubscription(API_ID, CLIENT_ID, PLAN_ID).thenReturn(Optional.of(fakeSubscriptionFromCache(false))); + + String clientCert = Files.readString( + Paths.get(Oauth2PolicyV3IntegrationTest.class.getResource("/certificate/client1-crt.pem").toURI()) + ); + String encoded = URLEncoder.encode(clientCert, Charset.defaultCharset()); + + client + .rxRequest(HttpMethod.GET, "/test") + .flatMap(request -> + request + .putHeader("Authorization", "Bearer " + DummyOAuth2Resource.TOKEN_SUCCESS_WITH_CNF) + .putHeader("ssl-client-cert", encoded) + .rxSend() + ) + .flatMapPublisher(response -> { + assertThat(response.statusCode()).isEqualTo(200); + return response.toFlowable(); + }) + .test() + .await() + .assertComplete() + .assertValue(body -> { + assertThat(body).hasToString("response from backend"); + return true; + }) + .assertNoErrors(); + + wiremock.verify(1, getRequestedFor(urlPathEqualTo("/team"))); + } + + @Test + void should_return_401_without_valid_certificate_in_header(HttpClient client) throws InterruptedException { + wiremock.stubFor(get("/team").willReturn(ok("response from backend"))); + + Single httpClientResponse = client + .rxRequest(HttpMethod.GET, "/test") + .flatMap(request -> + request + .putHeader("Authorization", "Bearer " + DummyOAuth2Resource.TOKEN_SUCCESS_WITH_CNF) + .putHeader("ssl-client-cert", "wrong") + .rxSend() + ); + + assert401unauthorized(wiremock, httpClientResponse); + } + + @Test + void should_return_401_with_valid_certificate_in_header_but_without_cnf_in_token(HttpClient client) + throws InterruptedException, URISyntaxException, IOException { + wiremock.stubFor(get("/team").willReturn(ok("response from backend"))); + + String clientCert = Files.readString( + Paths.get(Oauth2PolicyV3IntegrationTest.class.getResource("/certificate/client1-crt.pem").toURI()) + ); + String encoded = URLEncoder.encode(clientCert, Charset.defaultCharset()); + + Single httpClientResponse = client + .rxRequest(HttpMethod.GET, "/test") + .flatMap(request -> + request + .putHeader("Authorization", "Bearer " + DummyOAuth2Resource.TOKEN_SUCCESS_WITH_CLIENT_ID) + .putHeader("ssl-client-cert", encoded) + .rxSend() + ); + + assert401unauthorized(wiremock, httpClientResponse); + } + + protected OngoingStubbing> whenSearchingSubscription(String api, String clientId, String plan) { + return when(getBean(SubscriptionService.class).getByApiAndSecurityToken(eq(api), securityTokenMatcher(clientId), eq(plan))); + } + } + + @Nested + @DeployApi("/apis/oauth2.json") + @GatewayTest + public class Oauth2PolicyV4EmulationEngineCnfPeerCertificateIntegrationTest + extends AbstractOauth2PolicyCnfPeerCertificateIntegrationTest {} + + public static class AbstractOauth2PolicyCnfPeerCertificateIntegrationTest + extends AbstractPolicyTest { + + @Override + public void configureGateway(GatewayConfigurationBuilder gatewayConfigurationBuilder) { + Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureGateway(gatewayConfigurationBuilder); + } + + @Override + protected void configureHttpClient(final HttpClientOptions options) { + Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureHttpClient(options, gatewayPort()); + + final PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions(); + pemKeyCertOptions.setCertPath(Oauth2PolicyV3IntegrationTest.class.getResource("/certificate/client1-crt.pem").getPath()); + pemKeyCertOptions.setKeyPath(Oauth2PolicyV3IntegrationTest.class.getResource("/certificate/client1-key.pem").getPath()); + options.setPemKeyCertOptions(pemKeyCertOptions); + } + + @Override + public void configureApi(final Api api) { + Plan oauth2Plan = createOauth2Plan(api); + + OAuth2PolicyConfiguration configuration = new OAuth2PolicyConfiguration(); + configuration.setOauthResource("dummy-oauth2-resource"); + configuration.getConfirmationMethodValidation().getCertificateBoundThumbprint().setEnabled(true); + try { + oauth2Plan.setSecurityDefinition(new ObjectMapper().writeValueAsString(configuration)); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to set OAuth2 policy configuration", e); + } + + api.setPlans(Collections.singletonList(oauth2Plan)); + } + + @Override + public void configureResources(final Map resources) { + Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureResources(resources); + } + + @Test + void should_access_api_with_valid_certificate_from_ssl_session(HttpClient client) + throws InterruptedException, URISyntaxException, IOException { + wiremock.stubFor(get("/team").willReturn(ok("response from backend"))); + + // subscription found is valid + whenSearchingSubscription(API_ID, CLIENT_ID, PLAN_ID).thenReturn(Optional.of(fakeSubscriptionFromCache(false))); + + client + .rxRequest(HttpMethod.GET, "/test") + .flatMap(request -> request.putHeader("Authorization", "Bearer " + DummyOAuth2Resource.TOKEN_SUCCESS_WITH_CNF).rxSend()) + .flatMapPublisher(response -> { + assertThat(response.statusCode()).isEqualTo(200); + return response.toFlowable(); + }) + .test() + .await() + .assertComplete() + .assertValue(body -> { + assertThat(body).hasToString("response from backend"); + return true; + }) + .assertNoErrors(); + + wiremock.verify(1, getRequestedFor(urlPathEqualTo("/team"))); + } + + @Test + void should_return_401_with_valid_certificate_from_ssl_session_but_without_cnf_in_token(HttpClient client) + throws InterruptedException, URISyntaxException, IOException { + wiremock.stubFor(get("/team").willReturn(ok("response from backend"))); + + Single httpClientResponse = client + .rxRequest(HttpMethod.GET, "/test") + .flatMap(request -> + request.putHeader("Authorization", "Bearer " + DummyOAuth2Resource.TOKEN_SUCCESS_WITH_CLIENT_ID).rxSend() + ); + + assert401unauthorized(wiremock, httpClientResponse); + } + + protected OngoingStubbing> whenSearchingSubscription(String api, String clientId, String plan) { + return when(getBean(SubscriptionService.class).getByApiAndSecurityToken(eq(api), securityTokenMatcher(clientId), eq(plan))); + } + } + + @Nested + @DeployApi("/apis/oauth2.json") + @GatewayTest + public class Oauth2PolicyV4EmulationEngineCnfInvalidPeerCertificateIntegrationTest + extends AbstractOauth2PolicyCnfInvalidPeerCertificateIntegrationTest {} + + public static class AbstractOauth2PolicyCnfInvalidPeerCertificateIntegrationTest + extends AbstractPolicyTest { + + @Override + public void configureGateway(GatewayConfigurationBuilder gatewayConfigurationBuilder) { + Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureGateway(gatewayConfigurationBuilder); + } + + @Override + protected void configureHttpClient(final HttpClientOptions options) { + Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureHttpClient(options, gatewayPort()); + } + + @Override + public void configureApi(final Api api) { + Plan oauth2Plan = createOauth2Plan(api); + + OAuth2PolicyConfiguration configuration = new OAuth2PolicyConfiguration(); + configuration.setOauthResource("dummy-oauth2-resource"); + configuration.getConfirmationMethodValidation().getCertificateBoundThumbprint().setEnabled(true); + try { + oauth2Plan.setSecurityDefinition(new ObjectMapper().writeValueAsString(configuration)); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to set OAuth2 policy configuration", e); + } + + api.setPlans(Collections.singletonList(oauth2Plan)); + } + + @Override + public void configureResources(final Map resources) { + Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureResources(resources); + } + + @Test + void should_return_401_without_valid_peer_certificate_from_ssl_session(HttpClient client) throws InterruptedException { + wiremock.stubFor(get("/team").willReturn(ok("response from backend"))); + + Single httpClientResponse = client + .rxRequest(HttpMethod.GET, "/test") + .flatMap(request -> + request.putHeader("Authorization", "Bearer " + DummyOAuth2Resource.TOKEN_SUCCESS_WITH_CLIENT_ID).rxSend() + ); + + assert401unauthorized(wiremock, httpClientResponse); + } + } +} diff --git a/src/test/java/io/gravitee/policy/v3/oauth2/Oauth2PolicyV3CnfIntegrationTest.java b/src/test/java/io/gravitee/policy/v3/oauth2/Oauth2PolicyV3CnfIntegrationTest.java new file mode 100644 index 00000000..d011aea0 --- /dev/null +++ b/src/test/java/io/gravitee/policy/v3/oauth2/Oauth2PolicyV3CnfIntegrationTest.java @@ -0,0 +1,90 @@ +/* + * 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.v3.oauth2; + +import static io.gravitee.definition.model.ExecutionMode.V3; +import static org.mockito.Mockito.when; + +import io.gravitee.apim.gateway.tests.sdk.annotations.DeployApi; +import io.gravitee.apim.gateway.tests.sdk.annotations.GatewayTest; +import io.gravitee.gateway.api.service.Subscription; +import io.gravitee.gateway.api.service.SubscriptionService; +import io.gravitee.policy.oauth2.Oauth2PolicyV4EmulationEngineCnfIntegrationTest; +import java.util.Optional; +import org.junit.jupiter.api.Nested; +import org.mockito.stubbing.OngoingStubbing; + +/** + * @author GraviteeSource Team + */ +public class Oauth2PolicyV3CnfIntegrationTest { + + @Nested + @DeployApi("/apis/oauth2.json") + @GatewayTest(v2ExecutionMode = V3) + public class Oauth2PolicyV3MissingCnfIntegrationTest + extends Oauth2PolicyV4EmulationEngineCnfIntegrationTest.AbstractOauth2PolicyMissingCnfIntegrationTest { + + /** + * This overrides subscription search : + * - in jupiter its searched with getByApiAndSecurityToken + * - in V3 its searches with api/clientId/plan + */ + protected OngoingStubbing> whenSearchingSubscription(String api, String clientId, String plan) { + return when(getBean(SubscriptionService.class).getByApiAndClientIdAndPlan(api, clientId, plan)); + } + } + + @Nested + @DeployApi("/apis/oauth2.json") + @GatewayTest(v2ExecutionMode = V3) + public class Oauth2PolicyV3CnfHeaderCertificateIntegrationTest + extends Oauth2PolicyV4EmulationEngineCnfIntegrationTest.AbstractOauth2PolicyCnfHeaderCertificateIntegrationTest { + + /** + * This overrides subscription search : + * - in jupiter its searched with getByApiAndSecurityToken + * - in V3 its searches with api/clientId/plan + */ + @Override + protected OngoingStubbing> whenSearchingSubscription(String api, String clientId, String plan) { + return when(getBean(SubscriptionService.class).getByApiAndClientIdAndPlan(api, clientId, plan)); + } + } + + @Nested + @DeployApi("/apis/oauth2.json") + @GatewayTest(v2ExecutionMode = V3) + public class Oauth2PolicyV3CnfPeerCertificateIntegrationTest + extends Oauth2PolicyV4EmulationEngineCnfIntegrationTest.AbstractOauth2PolicyCnfPeerCertificateIntegrationTest { + + /** + * This overrides subscription search : + * - in jupiter its searched with getByApiAndSecurityToken + * - in V3 its searches with api/clientId/plan + */ + @Override + protected OngoingStubbing> whenSearchingSubscription(String api, String clientId, String plan) { + return when(getBean(SubscriptionService.class).getByApiAndClientIdAndPlan(api, clientId, plan)); + } + } + + @Nested + @DeployApi("/apis/oauth2.json") + @GatewayTest(v2ExecutionMode = V3) + public class Oauth2PolicyV3CnfInvalidPeerCertificateIntegrationTest + extends Oauth2PolicyV4EmulationEngineCnfIntegrationTest.AbstractOauth2PolicyCnfInvalidPeerCertificateIntegrationTest {} +} diff --git a/src/test/resources/certificate/ca-crt.pem b/src/test/resources/certificate/ca-crt.pem new file mode 100644 index 00000000..65c6030c --- /dev/null +++ b/src/test/resources/certificate/ca-crt.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICpjCCAY4CCQC3vXoVDq9I4TANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls +b2NhbGhvc3QwIBcNMjMwODMwMDY0NzU0WhgPMjA1MTAxMTQwNjQ3NTRaMBQxEjAQ +BgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AJnhhNZEUwAt1CPo2lgvz0qntq+VeQkj/AtQa0vlHRtTKdGmlJitlOfvnvVCS+Dm +bBt92mY9ej/yeA+e4G3V6fnpoVs/IeoTzOaw/99VrMSH9qdMHwv9V6JS2mCN7Lx9 +M2uApBc9GBiWy2UpPForL/04MJ0FdBkC/QxSKJu3w5cpyoSzMaFEa1rVrabqEZW6 +Y5ctwq4wtnnXT88hW/ioqRhw9KgcA2GExDAEKKPZlYAetSLRGEz1D/+/5kiRi8EJ +F9U9bEokMCi2d3S3th/Q+b9KwIQ6AT/yOQfgkivNaNEiWldbtZ47m3vF+lfhG2T1 +mZPGsbzaDEJqTXrgLTT6cIkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAGZrVurBx +zV7NRgY+s3snZL5UXaSmrMCVEd238QhGSNbp/bA1Mvzo89kRSnttZ7u4EckmEtXa +HMEV26DS/LiaRfj+EV3NPAWxhLf6458n2wQbLTz3tUyHMo44lC/I1BGS0myKM4L2 +lS4GH6I+MP94LJt07UHiRndyiO32svYgkCBT0VEPKLsT5E3s7wwyFX/eUrubZut4 +J3P4DgJ37fGZvGWQ8oVAHK3wa+lynYUXkAz9u15ZAU8YtSjShHwJxHQi50h0SD5A +5To6YsPAvKGGeJtzCBI/J4/6Rkj+6hcyDEHrmzwbjqVVw5KZnRfQOL7UwgfaeFHk +O996ZP0BXOTg5Q== +-----END CERTIFICATE----- diff --git a/src/test/resources/certificate/ca-crt.srl b/src/test/resources/certificate/ca-crt.srl new file mode 100644 index 00000000..7d623602 --- /dev/null +++ b/src/test/resources/certificate/ca-crt.srl @@ -0,0 +1 @@ +C9A98094B1A3B2E4 diff --git a/src/test/resources/certificate/ca-key.pem b/src/test/resources/certificate/ca-key.pem new file mode 100644 index 00000000..7a91f158 --- /dev/null +++ b/src/test/resources/certificate/ca-key.pem @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFHzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIRhlG0vdvRLUCAggA +MB0GCWCGSAFlAwQBKgQQe3ObWQqFqzsx4fpvLj1LewSCBNA5E4iYa4XgTX4vcfFB +GFBathkSBkmg/J2G+TWNethFCglFVjVsrwtX4VSOoBnyo3Ab0o9Sog/5zLHvrnWw +rL0rbRl8AqfqJaiGGaG4nkFIq1HLDeu0W6/6p6eLCg5lSK/r4xzxN2YIPrghwdPo +BTt/K85Kvd0MnWzIukMCECvHmQAwzqLBKdSnuNco0Hb75XzayolZuT3sLwLApdhO +chPuI+FX/Kc3NnTB8/5xT3JdYk7lBlJMCBECf+Km7v5kpAgqYnKF44lTBTfm3MyZ +cdbcgDCq9jRBJHFuI8xNhSkD83fu4xUE4VHaA08lxmZI58xLnPnTh9XpI2jDlnoK +UxHaB2/7JPZFTrDxcqxWgEFvx/FulQQhteA6bg5Rh8zF6tNz4bvZBUbnIVgH24pY +psXSDMGMADJateqbA3JQ8gTzAIjS5FOKMw5ISFcMdqCkNORXJEeP11UClrEOx49q +ww0Tyn1ZZb3LqOrHkkDx186w2lsvwXF6med88Z7CcK+QQK+S9vVIhRINJDotyXv5 +GCmTwLoKqk4Uhn0wLFfrS+4Q4BSAEbBcigvncYK+xoKKnHAmMtx2E3XC6gDkxAnW +uADQmFEKmJDvR++UYb35fYIWUnUXXycxo0OXL2KapREMPhLu73X3BlmFrfDdvqB8 +yXO8nT4M8RNIi+IpRBN6ORM+V4qxMmzGYfcKcHBuzZQSMf04wh1dsPSM2Q3g08pI +wRbOknsu3haGpD+CGJ3/iXpHUveWM+kZ1MiGMJ5k+EvGf8ugf05BIduqRiJ6/8Yk +DhakmXyVSMG2jpLy10ukL/cVVPXYs/n42gJd9JfI2++B6rvgg/gNjBb7O9s51kXI +r+T8fB1OdlUny9i+YScYAD2ac7Mvs4CmcRHMezuLNtWknzgjUTUOVQlaKCbxenFa +HgplS4yRSeA7ikaAowID0+1Z3UGtl8jh+/FQk7SGg01ddLmcRvvMpAONDdQ3aGPj +BXD5BuyW1sjL4c4qcxwVRQoRrlI8Mitd2P4x1P5OMQEcGPlQXqnGBirxxqqqj2Lp +XCZoLXKQ/7zfxzjXDAGKkQTD4yNiA+u4ZljSm6rsuOTYWzBc6i2/SMPwqa8mb71x +Je1DEDu79Kx3bwO9Qb8Bzm4d9x3oDbfGn15HAMOnrpdOFe+5asWnksXv7Z3gpU0w +LhuqjLMVCpROPmXycLoQpHx3c2exc/efZIAvzaEzUzzTeAKUiI62v8ywu+jo7gNp +OvC/B8i3OdJJGJtAz5/im5h1Zsk9HGtisS7fDN4cLk0EwgTeCQAFt7sOyXgyZ6rj +kXD0vY4w2sYuecDD/430G4m4EoeQQnlViz2cA5ApunXovbhHGRALOdpXPX77K1ME +bsCQeqKhqVqvVyy7m53V2jbSXFshraoyIv8d2SKq9ymrLIrqVumkzSkdA+Ciki66 +T5otBP4JwgARZag25bUIL7Zz8zGpKmBAbnw947XS5EU0BrpwWyLxXSA25HETUi3e +n11hLYQLtlFVoLWmIbyZPsfbJ7veuMxMxK5aXQvHHSyJ92Pa9xcMsdzvxDexHCgs +ODnwz+adJLIxTRPRJ3g54StckK5TDjBaFQhyisCahSosOZpiDX7fccBiVEStCfD3 +iFHRc/q7BwGyOEHIhvyn2y9xng== +-----END ENCRYPTED PRIVATE KEY----- diff --git a/src/test/resources/certificate/client1-crt.pem b/src/test/resources/certificate/client1-crt.pem new file mode 100644 index 00000000..7cf6209b --- /dev/null +++ b/src/test/resources/certificate/client1-crt.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDpjCCAo4CCQDJqYCUsaOy5DANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls +b2NhbGhvc3QwIBcNMjMwODMwMDY0ODA5WhgPMjA1MTAxMTQwNjQ4MDlaMBQxEjAQ +BgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +AMt4R19sPi99A7sAYoDlA645j+yiLL7Ope7rOlndVn8yzILpTFPq4uv3j2Kuq46O +3bbfoEKVIdkpYTzG/wBi1L7uedpA37Fe/qkR/TWYweFl07cHg5rAqiaqaPAmR6wv +sAG48ZCyp5MsSEVjXwGWyRioJlwVcXunlD74BGV1KtWaqhYbN+r6l0r/wdbkKXBk +VylPbGTW47Lct0iWt0nxzE4G+WhGsU7TT6yrqbs33vIdpMQRnjqa+6xXoOp4wHu/ +6HXBwTuEhXFqfk3YYO78ZN4tiYBwZDbR4RjQOSCWZgYO6ND1U7RnUcEcNnR7Gyyq +AoOB1pwGldch7GCEUk5PbrJEINtTHH1aoljQXbOfkStrvM62A2NrEbuRgTPFczi7 +7dUyzXwjOKh2ZYfRGEEGgxeHwboe03ePXxVjEC1lQgCd+VDcqyC/OvQvQRBk74Ew +V+8NWimDlFdQ3sw0Iqahr5RiCsop5KASNeD8bSExaXQ2gEAHvZiqG2diBjuZn+9x +HRh/bpRVo70JC7Qyx11YVaiumtgEQyMrDsW8GTsr3PMf9cghcbDsPj5wWRiDWnfn +KM+PDlW4tFgS6hc87Q8zSDvtEf9qZxShsfVtmA3qOZEQOghRbRauyA3NWeyXJeWn +iZDjk282Sf/5/L/23sc9GN+k4y3hRUBuHSBP9YyPwvxVAgMBAAEwDQYJKoZIhvcN +AQELBQADggEBAGm7j6QtYHKMq/o14KBScsdpyjAEkI5YI+PwHjcgydPp50hvqouA +bSNysjh5ufzhVOm7W9EvG++3cikl6yowKjVmNtOsxAVBdG6MXrT8vkbz5TVfyjbj +IkwJl1Xt32XesFuxXLmO7XPgS0RrROWPBzYAt8TsbI2SzBI2k860gBRup614RfTF +lLnPYFOIoUjtz9Nn96prVfjOwDk7jqdEj6Oh0z4DBtBZY2HmZ6QAc5cf/CPpb4WB +N2gYxrwppTUq8EOzj1aFbacIXwO78XknRDHzgeGxr9LWSBy+vpqjo1BK+DpJwx4o +LlgM3YAcXb6kyPkxzjwQzJ1P8uCNIocAmEs= +-----END CERTIFICATE----- diff --git a/src/test/resources/certificate/client1-csr.pem b/src/test/resources/certificate/client1-csr.pem new file mode 100644 index 00000000..c6c3de1c --- /dev/null +++ b/src/test/resources/certificate/client1-csr.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIEWTCCAkECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAy3hHX2w+L30DuwBigOUDrjmP7KIsvs6l7us6Wd1W +fzLMgulMU+ri6/ePYq6rjo7dtt+gQpUh2SlhPMb/AGLUvu552kDfsV7+qRH9NZjB +4WXTtweDmsCqJqpo8CZHrC+wAbjxkLKnkyxIRWNfAZbJGKgmXBVxe6eUPvgEZXUq +1ZqqFhs36vqXSv/B1uQpcGRXKU9sZNbjsty3SJa3SfHMTgb5aEaxTtNPrKupuzfe +8h2kxBGeOpr7rFeg6njAe7/odcHBO4SFcWp+Tdhg7vxk3i2JgHBkNtHhGNA5IJZm +Bg7o0PVTtGdRwRw2dHsbLKoCg4HWnAaV1yHsYIRSTk9uskQg21McfVqiWNBds5+R +K2u8zrYDY2sRu5GBM8VzOLvt1TLNfCM4qHZlh9EYQQaDF4fBuh7Td49fFWMQLWVC +AJ35UNyrIL869C9BEGTvgTBX7w1aKYOUV1DezDQipqGvlGIKyinkoBI14PxtITFp +dDaAQAe9mKobZ2IGO5mf73EdGH9ulFWjvQkLtDLHXVhVqK6a2ARDIysOxbwZOyvc +8x/1yCFxsOw+PnBZGINad+coz48OVbi0WBLqFzztDzNIO+0R/2pnFKGx9W2YDeo5 +kRA6CFFtFq7IDc1Z7Jcl5aeJkOOTbzZJ//n8v/bexz0Y36TjLeFFQG4dIE/1jI/C +/FUCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4ICAQDD6rezlVJfBNk582l3zZiYGdxA +p0CHe8dmuryVk94OxaVOCSiWl550jPFYjZNb/7Z9/tau/zhJ+JyQT9u8l2XVgLuj +pLDFWJmz6Gtv0TBrFEa1V6uTu7INBxqWxJDBh9QviwvzetVxT4+PkpNbBjY8E57H +Nif59nha92a31J/Jytx6/X4GZNJCEJtFK4hqNZNhnsrc91+tqecHMJatvj7YxB38 +VQYOK64+iJ24v6r7bMuE9aAE8M6bJmRPupzL0Fl7yXISqrhYryeRrCXHyRVhUByh +XiP5ycxKuMByuZz52S7N2qbpMvD7zO8lsNrYDA3Ecz4BmGXJ7uOHA00GnA4eRJJT +IOKbpGHLo2j5WYLGetR1aBODH5gj6Kkg/oo+o/FazmDiv5toB5ltzemZ7+QtBgN4 +6Xs421zj4NYHN6kLR0O23cDiWbib9PuwJnCYNqdthOw7ykQVBkcE6mdm+dvdO3BF +0SEajSz/l1jJX9AQWRWopHR074QISP8EIgYXFJgYPVytMStdnfg7Z7/NgMgUQ0jD +QZyWv/3RLkviYUks3VvsE4qq/4FeFwb9dB4tIo1wMgQXve7e8prJIkQ5X3CiJUuJ +YT3E+p+/Hg9IY1QjJ5wCsmwlwnX3Q6eptGePa5BPhB3XVTMrMTnmwKxf58xKwXyH +9yQkb/jT834YKzsFJg== +-----END CERTIFICATE REQUEST----- diff --git a/src/test/resources/certificate/client1-key.pem b/src/test/resources/certificate/client1-key.pem new file mode 100644 index 00000000..f45a17c1 --- /dev/null +++ b/src/test/resources/certificate/client1-key.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/src/test/resources/certificate/keystore.p12 b/src/test/resources/certificate/keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..192fa482cf40393aff87eea13ad1628a7ea80965 GIT binary patch literal 2285 zcmY+EXEYm*9)^X`7$s)Z-h?9NrGko4sznofwDyepBSsY^R8!PmrDD`pTPZ525xYha zyESUmsFvcU(%YVM@BQEV;dg%LJnuR0x95dnK|9m{8WM+O{fG;Q&WSe zqOdd|j~H9#?4W#~Yl5IppTH|?)#(Mf!5JDFdACG7n2Db|p?Y);{xu{>M*8xMHUHSf zdy=RQmE-|cpy$&RNt$~Ctu^)J`UFNhJN!>hPXsY7G%13Mdq<&Urd7Z2MFiq%863PZU8$#tS?*TS>xVGG0HSbVR2td?b15j~K(GzWGCCaO&_S*}{!{RBi} zlHV(-u%_`Qb$s_5$_oy~~aBk<4aaFR;Nl@v1mr@(M}s)%n%t1v?&OT{6~) zzNgbbJ~Ar1joF_FssHJIrZSaauLh`~g!v!!nxdZ6_P+F&Ro6e(%8-z5!zujfBYjZp|)xnb&wpXd!hB620c@OJ}Au z84G3XA~ICx?v9e|hPZg@qfVgkgyqhf6R$u>%Q51fjkcBzz>Pa*qi+}~Ik}KVG@9s6AEiEHfQH?|Uy~7})i{W+TquvsfQnm^@`&&`#F*M`@QdY@ zlhLO1=1tcr(oOFYPJxcf-x_T})gNko`6^~v`t@$KA>k#%kkDJJ6liD7XLx9&ho=Y%eQks_8mh}RT(3tR@|gq&4OoNH;3>z zJHjtrjC{{mgFnD-0I!!tYZGQtMcHhEriIcIj4!<8!R?Ll8B!Ztm2u>UTViq-Q@n9F zdycjuwC;WaG*fEDRxte|IMbvyv0Nrm;N7~EVC2!tK<-#+X3KJ!b9RKhEPue2tV@O? z6<19%yb63Re2l7f=UfCUrOw)zJd0UIuxmzzZDXNP$HwqX{71`+oN|ij{r2w16V8Vz zvw|Wo7|!EgEh>QXub-$D8nAX4B?rkJ%PnSv1^xB-kew3ijA@H1;iwe%8t;~_JS4lF zeO}3Nm?sY5hsjIi+^G`r5%9}^VPsdeN6Gs%<(++DA)ipcyB>LHgeGfb`^aqg&8`TzKy3IM|IFH(gYu*w z>CfhqAV-j~HWbs>F0xQ~Np>7-Taf`(!9~CM3~jeFfpc>Cj88r02iOzJoXH}jG>t>$ z=tD4m5B3fV*lV)jZ{+(~GQMAEaZ4MBwpanSGOfp()FDgO~C_Z`TVwrw_ zd7psWc(Wa2EllSb?%zlaFYMN(e2-^Mvdw4F1=S#rDXid4iSu zcH-J!$r|39AJia0=1ck9nCr=qxWL$Wq%EQS%6IRaRw|eLt-#3VuX@U+QF<_a zoCce?aK&NqzB$>UWPRcG#6i4Q?98?69$fWd_!g$HD?+3HunKf_@#nP?GOLO9kWz4POFW^3KjED$`x!jA!j*(2BeQ1lfk%@0Dib>in~k F{{rP*H)j9< literal 0 HcmV?d00001