diff --git a/pom.xml b/pom.xml
index 26da3423..8448c2c1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -41,7 +41,6 @@
1.70
1.0.1
- 1.3.2
3.6.0
0.19
@@ -145,14 +144,6 @@
${gravitee-json-validation.version}
test
-
-
- org.apache.commons
- commons-io
- ${commons-io.version}
- test
-
-
diff --git a/src/main/java/io/gravitee/policy/sslenforcement/SslEnforcementPolicy.java b/src/main/java/io/gravitee/policy/sslenforcement/SslEnforcementPolicy.java
index a5624451..b0d0c57d 100644
--- a/src/main/java/io/gravitee/policy/sslenforcement/SslEnforcementPolicy.java
+++ b/src/main/java/io/gravitee/policy/sslenforcement/SslEnforcementPolicy.java
@@ -19,24 +19,30 @@
import io.gravitee.common.util.Maps;
import io.gravitee.gateway.api.Request;
import io.gravitee.gateway.api.Response;
+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;
+import io.gravitee.policy.sslenforcement.configuration.CertificateLocation;
import io.gravitee.policy.sslenforcement.configuration.SslEnforcementPolicyConfiguration;
+import java.io.ByteArrayInputStream;
+import java.net.URLDecoder;
+import java.nio.charset.Charset;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Optional;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.security.auth.x500.X500Principal;
-import org.bouncycastle.asn1.ASN1ObjectIdentifier;
-import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
-import org.bouncycastle.asn1.x500.RDN;
+import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.asn1.x500.X500Name;
-import org.bouncycastle.asn1.x500.style.IETFUtils;
-import org.springframework.util.AntPathMatcher;
+import org.springframework.util.StringUtils;
/**
* @author David BRASSELY (david.brassely at graviteesource.com)
* @author GraviteeSource Team
*/
+@Slf4j
public class SslEnforcementPolicy {
private final SslEnforcementPolicyConfiguration configuration;
@@ -70,13 +76,8 @@ public void onRequest(Request request, Response response, PolicyChain policyChai
return;
}
- X500Principal peerPrincipal = null;
-
- try {
- peerPrincipal = (X500Principal) sslSession.getPeerPrincipal();
- } catch (SSLPeerUnverifiedException e) {}
-
- if (configuration.isRequiresClientAuthentication() && peerPrincipal == null) {
+ var principal = extractX500Principal(request);
+ if (configuration.isRequiresClientAuthentication() && principal == null) {
policyChain.failWith(PolicyResult.failure(AUTHENTICATION_REQUIRED, HttpStatusCode.UNAUTHORIZED_401, "Unauthorized"));
return;
@@ -87,7 +88,7 @@ public void onRequest(Request request, Response response, PolicyChain policyChai
configuration.getWhitelistClientCertificates() != null &&
!configuration.getWhitelistClientCertificates().isEmpty()
) {
- X500Name peerName = new X500Name(peerPrincipal.getName());
+ X500Name peerName = new X500Name(principal.getName());
boolean found = false;
@@ -95,7 +96,7 @@ public void onRequest(Request request, Response response, PolicyChain policyChai
// Prepare name with javax.security to transform to valid bouncycastle Asn1ObjectIdentifier
final X500Principal x500Principal = new X500Principal(name);
final X500Name x500Name = new X500Name(x500Principal.getName());
- found = areEqual(x500Name, peerName);
+ found = X500NameComparator.areEqual(x500Name, peerName);
if (found) {
break;
@@ -108,7 +109,7 @@ public void onRequest(Request request, Response response, PolicyChain policyChai
CLIENT_FORBIDDEN,
HttpStatusCode.FORBIDDEN_403,
"You're not allowed to access this resource",
- Maps.builder().put("name", peerPrincipal.getName()).build()
+ Maps.builder().put("name", principal.getName()).build()
)
);
@@ -119,107 +120,48 @@ public void onRequest(Request request, Response response, PolicyChain policyChai
policyChain.doNext(request, response);
}
- private boolean areEqual(X500Name name1, X500Name name2) {
- final RDN[] rdns1 = name1.getRDNs();
- final RDN[] rdns2 = name2.getRDNs();
-
- if (rdns1.length != rdns2.length) {
- return false;
- }
-
- boolean reverse = false;
-
- if (rdns1[0].getFirst() != null && rdns2[0].getFirst() != null) {
- reverse = !rdns1[0].getFirst().getType().equals(rdns2[0].getFirst().getType()); // guess forward
- }
-
- for (int i = 0; i != rdns1.length; i++) {
- if (!foundMatch(reverse, rdns1[i], rdns2)) {
- return false;
- }
- }
-
- return true;
- }
+ private X500Principal extractX500Principal(Request request) {
+ if (configuration.getCertificateLocation() == CertificateLocation.SESSION) {
+ SSLSession sslSession = request.sslSession();
- private boolean foundMatch(boolean reverse, RDN rdn, RDN[] possRDNs) {
- if (reverse) {
- for (int i = possRDNs.length - 1; i >= 0; i--) {
- if (possRDNs[i] != null && rDNAreEqual(rdn, possRDNs[i])) {
- possRDNs[i] = null;
- return true;
- }
- }
- } else {
- for (int i = 0; i != possRDNs.length; i++) {
- if (possRDNs[i] != null && rDNAreEqual(rdn, possRDNs[i])) {
- possRDNs[i] = null;
- return true;
+ if (null != sslSession) {
+ try {
+ return (X500Principal) sslSession.getPeerPrincipal();
+ } catch (SSLPeerUnverifiedException e) {
+ return null;
}
}
+ return null;
}
- return false;
+ return extractCertificate(request.headers(), configuration.getCertificateHeaderName())
+ .map(X509Certificate::getSubjectX500Principal)
+ .orElse(null);
}
- private static boolean rDNAreEqual(RDN rdn1, RDN rdn2) {
- if (rdn1.isMultiValued()) {
- if (rdn2.isMultiValued()) {
- AttributeTypeAndValue[] atvs1 = rdn1.getTypesAndValues();
- AttributeTypeAndValue[] atvs2 = rdn2.getTypesAndValues();
+ public static Optional extractCertificate(final HttpHeaders httpHeaders, final String certHeader) {
+ Optional certificate = Optional.empty();
- if (atvs1.length != atvs2.length) {
- return false;
- }
+ String certHeaderValue = StringUtils.hasText(certHeader) ? httpHeaders.get(certHeader) : null;
- for (int i = 0; i != atvs1.length; i++) {
- if (!atvAreEqual(atvs1[i], atvs2[i])) {
- return false;
- }
+ if (certHeaderValue != null) {
+ try {
+ if (!certHeaderValue.contains("\n")) {
+ certHeaderValue = URLDecoder.decode(certHeaderValue, Charset.defaultCharset());
}
- } else {
- return false;
+ certHeaderValue = certHeaderValue.replaceAll("\t", "\n");
+ CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+ certificate =
+ Optional.ofNullable(
+ (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(certHeaderValue.getBytes()))
+ );
+ } catch (Exception e) {
+ log.debug("Unable to retrieve peer certificate from request header '{}'", certHeader, e);
}
} else {
- if (!rdn2.isMultiValued()) {
- return atvAreEqual(rdn1.getFirst(), rdn2.getFirst());
- } else {
- return false;
- }
- }
-
- return true;
- }
-
- private static boolean atvAreEqual(AttributeTypeAndValue atv1, AttributeTypeAndValue atv2) {
- if (atv1 == atv2) {
- return true;
- }
-
- if (atv1 == null) {
- return false;
- }
-
- if (atv2 == null) {
- return false;
- }
-
- ASN1ObjectIdentifier o1 = atv1.getType();
- ASN1ObjectIdentifier o2 = atv2.getType();
-
- if (!o1.equals(o2)) {
- return false;
- }
-
- String v1 = IETFUtils.canonicalize(IETFUtils.valueToString(atv1.getValue()));
- String v2 = IETFUtils.canonicalize(IETFUtils.valueToString(atv2.getValue()));
-
- AntPathMatcher matcher = new AntPathMatcher();
-
- if (!matcher.match(v1, v2)) {
- return false;
+ log.debug("Header '{}' missing, unable to retrieve client certificate", certHeader);
}
- return true;
+ return certificate;
}
}
diff --git a/src/main/java/io/gravitee/policy/sslenforcement/X500NameComparator.java b/src/main/java/io/gravitee/policy/sslenforcement/X500NameComparator.java
new file mode 100644
index 00000000..d9c07311
--- /dev/null
+++ b/src/main/java/io/gravitee/policy/sslenforcement/X500NameComparator.java
@@ -0,0 +1,128 @@
+/**
+ * Copyright (C) 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.sslenforcement;
+
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
+import org.bouncycastle.asn1.x500.RDN;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.style.IETFUtils;
+import org.springframework.util.AntPathMatcher;
+
+public class X500NameComparator {
+
+ private X500NameComparator() {}
+
+ public static boolean areEqual(X500Name name1, X500Name name2) {
+ final RDN[] rdns1 = name1.getRDNs();
+ final RDN[] rdns2 = name2.getRDNs();
+
+ if (rdns1.length != rdns2.length) {
+ return false;
+ }
+
+ boolean reverse = false;
+
+ if (rdns1[0].getFirst() != null && rdns2[0].getFirst() != null) {
+ reverse = !rdns1[0].getFirst().getType().equals(rdns2[0].getFirst().getType()); // guess forward
+ }
+
+ for (int i = 0; i != rdns1.length; i++) {
+ if (!foundMatch(reverse, rdns1[i], rdns2)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static boolean foundMatch(boolean reverse, RDN rdn, RDN[] possRDNs) {
+ if (reverse) {
+ for (int i = possRDNs.length - 1; i >= 0; i--) {
+ if (possRDNs[i] != null && rDNAreEqual(rdn, possRDNs[i])) {
+ possRDNs[i] = null;
+ return true;
+ }
+ }
+ } else {
+ for (int i = 0; i != possRDNs.length; i++) {
+ if (possRDNs[i] != null && rDNAreEqual(rdn, possRDNs[i])) {
+ possRDNs[i] = null;
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static boolean rDNAreEqual(RDN rdn1, RDN rdn2) {
+ if (rdn1.isMultiValued()) {
+ if (rdn2.isMultiValued()) {
+ AttributeTypeAndValue[] atvs1 = rdn1.getTypesAndValues();
+ AttributeTypeAndValue[] atvs2 = rdn2.getTypesAndValues();
+
+ if (atvs1.length != atvs2.length) {
+ return false;
+ }
+
+ for (int i = 0; i != atvs1.length; i++) {
+ if (!atvAreEqual(atvs1[i], atvs2[i])) {
+ return false;
+ }
+ }
+ } else {
+ return false;
+ }
+ } else {
+ if (!rdn2.isMultiValued()) {
+ return atvAreEqual(rdn1.getFirst(), rdn2.getFirst());
+ } else {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static boolean atvAreEqual(AttributeTypeAndValue atv1, AttributeTypeAndValue atv2) {
+ if (atv1 == atv2) {
+ return true;
+ }
+
+ if (atv1 == null) {
+ return false;
+ }
+
+ if (atv2 == null) {
+ return false;
+ }
+
+ ASN1ObjectIdentifier o1 = atv1.getType();
+ ASN1ObjectIdentifier o2 = atv2.getType();
+
+ if (!o1.equals(o2)) {
+ return false;
+ }
+
+ String v1 = IETFUtils.canonicalize(IETFUtils.valueToString(atv1.getValue()));
+ String v2 = IETFUtils.canonicalize(IETFUtils.valueToString(atv2.getValue()));
+
+ AntPathMatcher matcher = new AntPathMatcher();
+
+ return matcher.match(v1, v2);
+ }
+}
diff --git a/src/main/java/io/gravitee/policy/sslenforcement/configuration/CertificateLocation.java b/src/main/java/io/gravitee/policy/sslenforcement/configuration/CertificateLocation.java
new file mode 100644
index 00000000..c30ec375
--- /dev/null
+++ b/src/main/java/io/gravitee/policy/sslenforcement/configuration/CertificateLocation.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright (C) 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.sslenforcement.configuration;
+
+import java.util.Map;
+import lombok.Getter;
+
+@Getter
+public enum CertificateLocation {
+ HEADER("header"),
+ SESSION("sessions");
+
+ private static final Map LABELS_MAP = Map.of(HEADER.label, HEADER, SESSION.label, SESSION);
+
+ private final String label;
+
+ CertificateLocation(String label) {
+ this.label = label;
+ }
+
+ public static CertificateLocation fromLabel(String label) {
+ if (label != null) {
+ return LABELS_MAP.get(label);
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/io/gravitee/policy/sslenforcement/configuration/SslEnforcementPolicyConfiguration.java b/src/main/java/io/gravitee/policy/sslenforcement/configuration/SslEnforcementPolicyConfiguration.java
index 35c8dbf1..e8ec23f0 100644
--- a/src/main/java/io/gravitee/policy/sslenforcement/configuration/SslEnforcementPolicyConfiguration.java
+++ b/src/main/java/io/gravitee/policy/sslenforcement/configuration/SslEnforcementPolicyConfiguration.java
@@ -17,41 +17,33 @@
import io.gravitee.policy.api.PolicyConfiguration;
import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
/**
* @author David BRASSELY (david.brassely at graviteesource.com)
* @author GraviteeSource Team
*/
+@Builder
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
public class SslEnforcementPolicyConfiguration implements PolicyConfiguration {
- private boolean requiresSsl;
+ @Builder.Default
+ private boolean requiresSsl = true;
private boolean requiresClientAuthentication;
- // Whitelist client certificates
+ /** Allowed client certificates (requires client authentication) **/
private List whitelistClientCertificates;
- public List getWhitelistClientCertificates() {
- return whitelistClientCertificates;
- }
+ @Builder.Default
+ private CertificateLocation certificateLocation = CertificateLocation.SESSION;
- public void setWhitelistClientCertificates(List whitelistClientCertificates) {
- this.whitelistClientCertificates = whitelistClientCertificates;
- }
-
- public boolean isRequiresSsl() {
- return requiresSsl;
- }
-
- public void setRequiresSsl(boolean requiresSsl) {
- this.requiresSsl = requiresSsl;
- }
-
- public boolean isRequiresClientAuthentication() {
- return requiresClientAuthentication;
- }
-
- public void setRequiresClientAuthentication(boolean requiresClientAuthentication) {
- this.requiresClientAuthentication = requiresClientAuthentication;
- }
+ /** Name of the header where to find the client certificate when using header certificate location **/
+ @Builder.Default
+ private String certificateHeaderName = "ssl-client-cert";
}
diff --git a/src/main/resources/schemas/schema-form.json b/src/main/resources/schemas/schema-form.json
index 7a52e795..8c426f3b 100644
--- a/src/main/resources/schemas/schema-form.json
+++ b/src/main/resources/schemas/schema-form.json
@@ -15,6 +15,19 @@
"type" : "boolean",
"default": false
},
+ "certificateLocation": {
+ "title": "Certificate location",
+ "description": "Location of the certificate.",
+ "type" : "string",
+ "enum": ["SESSION", "HEADER"],
+ "default": "SESSION"
+ },
+ "certificateHeaderName": {
+ "title": "Header name",
+ "description": "Name of the header where to find the client certificate.",
+ "type": "string",
+ "default": "ssl-client-cert"
+ },
"whitelistClientCertificates" : {
"type" : "array",
"title": "Allowed client certificates (requires client authentication).",
@@ -26,4 +39,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/test/java/io/gravitee/policy/sslenforcement/SslEnforcementPolicyTest.java b/src/test/java/io/gravitee/policy/sslenforcement/SslEnforcementPolicyTest.java
index 95f2fc2a..f2999a63 100644
--- a/src/test/java/io/gravitee/policy/sslenforcement/SslEnforcementPolicyTest.java
+++ b/src/test/java/io/gravitee/policy/sslenforcement/SslEnforcementPolicyTest.java
@@ -15,32 +15,40 @@
*/
package io.gravitee.policy.sslenforcement;
-import static org.mockito.ArgumentMatchers.argThat;
+import static java.util.Objects.requireNonNull;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import io.gravitee.gateway.api.Request;
import io.gravitee.gateway.api.Response;
+import io.gravitee.gateway.api.http.HttpHeaders;
import io.gravitee.policy.api.PolicyChain;
+import io.gravitee.policy.api.PolicyResult;
+import io.gravitee.policy.sslenforcement.configuration.CertificateLocation;
import io.gravitee.policy.sslenforcement.configuration.SslEnforcementPolicyConfiguration;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.Collections;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.security.auth.x500.X500Principal;
+import lombok.SneakyThrows;
+import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class SslEnforcementPolicyTest {
- private SslEnforcementPolicy policy;
-
- @Mock
- private SslEnforcementPolicyConfiguration configuration;
-
@Mock
private Request request;
@@ -53,125 +61,140 @@ class SslEnforcementPolicyTest {
@Mock
protected PolicyChain policyChain;
+ @Captor
+ protected ArgumentCaptor resultCaptor;
+
@BeforeEach
void init() {
when(request.sslSession()).thenReturn(sslSession);
-
- policy = new SslEnforcementPolicy(configuration);
- }
-
- @Test
- void shouldGoToNextPolicy() {
- policy.onRequest(request, response, policyChain);
-
- verify(policyChain).doNext(request, response);
}
@Test
- void shouldFail_requiresSsl_withoutSession() {
- when(configuration.isRequiresSsl()).thenReturn(true);
- when(request.sslSession()).thenReturn(null);
-
- policy.onRequest(request, response, policyChain);
-
- verify(policyChain).failWith(argThat(result -> SslEnforcementPolicy.SSL_REQUIRED.equals(result.key())));
- }
+ void should_go_to_next_policy_when_require_ssl_is_disabled() {
+ var configuration = SslEnforcementPolicyConfiguration.builder().requiresSsl(false).build();
- @Test
- void shouldFail_requiresClientAuthentication_withoutSession() {
- when(configuration.isRequiresSsl()).thenReturn(true);
- when(configuration.isRequiresClientAuthentication()).thenReturn(true);
-
- policy.onRequest(request, response, policyChain);
-
- verify(policyChain).failWith(argThat(result -> SslEnforcementPolicy.AUTHENTICATION_REQUIRED.equals(result.key())));
- }
+ new SslEnforcementPolicy(configuration).onRequest(request, response, policyChain);
- @Test
- void shouldFail_whitelistClientCertificate_unknownClient() throws SSLPeerUnverifiedException {
- when(configuration.isRequiresSsl()).thenReturn(true);
- when(configuration.isRequiresClientAuthentication()).thenReturn(true);
- when(configuration.getWhitelistClientCertificates())
- .thenReturn(Collections.singletonList("CN=Duke,OU=JavaSoft,O=Sun Microsystems,C=US"));
- when(sslSession.getPeerPrincipal()).thenReturn(new X500Principal("CN=Unknown"));
-
- policy.onRequest(request, response, policyChain);
-
- verify(policyChain).failWith(argThat(result -> SslEnforcementPolicy.CLIENT_FORBIDDEN.equals(result.key())));
+ verify(policyChain).doNext(request, response);
}
- @Test
- void shouldFail_whitelistClientCertificate_validClient() throws SSLPeerUnverifiedException {
- when(configuration.isRequiresSsl()).thenReturn(true);
- when(configuration.isRequiresClientAuthentication()).thenReturn(true);
- when(configuration.getWhitelistClientCertificates())
- .thenReturn(Collections.singletonList("CN=Duke,OU=JavaSoft,O=Sun Microsystems,C=US"));
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "CN=Duke,OU=JavaSoft,O=Sun Microsystems,C=US",
+ "C=US, O=Sun Microsystems, OU=JavaSoft, CN=Duke",
+ "C=US, O=Sun Microsystems, CN=Duke, OU=JavaSoft",
+ }
+ )
+ void should_go_to_next_policy_when_consumer_certificate_in_session_is_in_the_whitelist(String whitelist)
+ throws SSLPeerUnverifiedException {
when(sslSession.getPeerPrincipal()).thenReturn(new X500Principal("CN=Duke,OU=JavaSoft,O=Sun Microsystems,C=US"));
+ var configuration = SslEnforcementPolicyConfiguration
+ .builder()
+ .requiresSsl(true)
+ .requiresClientAuthentication(true)
+ .whitelistClientCertificates(Collections.singletonList(whitelist))
+ .build();
- policy.onRequest(request, response, policyChain);
+ new SslEnforcementPolicy(configuration).onRequest(request, response, policyChain);
verify(policyChain).doNext(request, response);
}
- @Test
- void shouldFail_whitelistClientCertificate_validClient_pattern() throws SSLPeerUnverifiedException {
- when(configuration.isRequiresSsl()).thenReturn(true);
- when(configuration.isRequiresClientAuthentication()).thenReturn(true);
- when(configuration.getWhitelistClientCertificates()).thenReturn(Collections.singletonList("CN=Duke,OU=JavaSoft,O=*,C=US"));
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "CN=Duke,OU=JavaSoft,O=*,C=US",
+ "CN=Duke, OU=JavaSoft, O=*, C=??",
+ "C=US, O=*, OU=JavaSoft, CN=Duke",
+ "C=??, O=*, OU=JavaSoft, CN=Duke",
+ }
+ )
+ void should_go_to_next_policy_when_consumer_certificate_in_session_match_to_pattern_in_the_whitelist(String pattern)
+ throws SSLPeerUnverifiedException {
when(sslSession.getPeerPrincipal()).thenReturn(new X500Principal("CN=Duke,OU=JavaSoft,O=Sun Microsystems,C=US"));
+ var configuration = SslEnforcementPolicyConfiguration
+ .builder()
+ .requiresSsl(true)
+ .requiresClientAuthentication(true)
+ .whitelistClientCertificates(Collections.singletonList(pattern))
+ .build();
- policy.onRequest(request, response, policyChain);
+ new SslEnforcementPolicy(configuration).onRequest(request, response, policyChain);
verify(policyChain).doNext(request, response);
}
- @Test
- void shouldFail_whitelistClientCertificate_validClient_pattern2() throws SSLPeerUnverifiedException {
- when(configuration.isRequiresSsl()).thenReturn(true);
- when(configuration.isRequiresClientAuthentication()).thenReturn(true);
- when(configuration.getWhitelistClientCertificates()).thenReturn(Collections.singletonList("CN=Duke,OU=JavaSoft,O=*,C=??"));
- when(sslSession.getPeerPrincipal()).thenReturn(new X500Principal("CN=Duke,OU=JavaSoft,O=Sun Microsystems,C=US"));
-
- policy.onRequest(request, response, policyChain);
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "CN=Duke,OU=JavaSoft,O=Sun Microsystems,C=US",
+ "C=US, O=Sun Microsystems, OU=JavaSoft, CN=Duke",
+ "C=US, O=Sun Microsystems, CN=Duke, OU=JavaSoft",
+ }
+ )
+ @SneakyThrows
+ void should_go_to_next_policy_when_consumer_certificate_in_header_is_in_the_whitelist(String whitelist) {
+ var certs = loadCertificate();
+ HttpHeaders headers = HttpHeaders.create().set("ssl-client-cert", certs);
+ when(request.headers()).thenReturn(headers);
+
+ var configuration = SslEnforcementPolicyConfiguration
+ .builder()
+ .requiresSsl(true)
+ .requiresClientAuthentication(true)
+ .whitelistClientCertificates(Collections.singletonList(whitelist))
+ .certificateLocation(CertificateLocation.HEADER)
+ .build();
+
+ new SslEnforcementPolicy(configuration).onRequest(request, response, policyChain);
verify(policyChain).doNext(request, response);
}
@Test
- void shouldFail_whitelistClientCertificate_reorder() throws SSLPeerUnverifiedException {
- when(configuration.isRequiresSsl()).thenReturn(true);
- when(configuration.isRequiresClientAuthentication()).thenReturn(true);
- when(configuration.getWhitelistClientCertificates()).thenReturn(Collections.singletonList("C=FR, O=GraviteeSource, CN=localhost"));
- when(sslSession.getPeerPrincipal()).thenReturn(new X500Principal("CN=localhost,O=GraviteeSource,C=FR"));
+ void should_fail_when_require_ssl_is_enabled_but_no_session() {
+ when(request.sslSession()).thenReturn(null);
+ var configuration = SslEnforcementPolicyConfiguration.builder().requiresSsl(true).build();
- policy.onRequest(request, response, policyChain);
+ new SslEnforcementPolicy(configuration).onRequest(request, response, policyChain);
- verify(policyChain).doNext(request, response);
+ verify(policyChain).failWith(resultCaptor.capture());
+ Assertions.assertThat(resultCaptor.getValue().key()).isEqualTo(SslEnforcementPolicy.SSL_REQUIRED);
}
@Test
- void shouldFail_whitelistClientCertificate_reorder_pattern() throws SSLPeerUnverifiedException {
- when(configuration.isRequiresSsl()).thenReturn(true);
- when(configuration.isRequiresClientAuthentication()).thenReturn(true);
- when(configuration.getWhitelistClientCertificates()).thenReturn(Collections.singletonList("C=FR, O=*, CN=localhost"));
- when(sslSession.getPeerPrincipal()).thenReturn(new X500Principal("CN=localhost,O=GraviteeSource,C=FR"));
+ @SneakyThrows
+ void should_fail_when_require_client_authentication_is_enabled_but_no_certifcate() {
+ when(sslSession.getPeerPrincipal()).thenReturn(null);
+ var configuration = SslEnforcementPolicyConfiguration.builder().requiresSsl(true).requiresClientAuthentication(true).build();
- policy.onRequest(request, response, policyChain);
+ new SslEnforcementPolicy(configuration).onRequest(request, response, policyChain);
- verify(policyChain).doNext(request, response);
+ verify(policyChain).failWith(resultCaptor.capture());
+ Assertions.assertThat(resultCaptor.getValue().key()).isEqualTo(SslEnforcementPolicy.AUTHENTICATION_REQUIRED);
}
@Test
- void shouldSuccess_withState() throws SSLPeerUnverifiedException {
- when(configuration.isRequiresSsl()).thenReturn(true);
- when(configuration.isRequiresClientAuthentication()).thenReturn(true);
- when(configuration.getWhitelistClientCertificates())
- .thenReturn(Collections.singletonList("C=FR, O=GraviteeSource, CN=localhost, OU=Eng, L=Lille 1, S=Lille"));
- when(sslSession.getPeerPrincipal())
- .thenReturn(new X500Principal("CN=localhost, O=GraviteeSource, C=FR, OU=Eng, L=Lille 1, S=Lille"));
+ @SneakyThrows
+ void should_fail_when_the_consumer_certificate_does_not_match_with_the_whitelist() {
+ when(sslSession.getPeerPrincipal()).thenReturn(new X500Principal("CN=Unknown"));
+ var configuration = SslEnforcementPolicyConfiguration
+ .builder()
+ .requiresSsl(true)
+ .requiresClientAuthentication(true)
+ .whitelistClientCertificates(Collections.singletonList("CN=Duke,OU=JavaSoft,O=Sun Microsystems,C=US"))
+ .build();
- policy.onRequest(request, response, policyChain);
+ new SslEnforcementPolicy(configuration).onRequest(request, response, policyChain);
- verify(policyChain).doNext(request, response);
+ verify(policyChain).failWith(resultCaptor.capture());
+ Assertions.assertThat(resultCaptor.getValue().key()).isEqualTo(SslEnforcementPolicy.CLIENT_FORBIDDEN);
+ }
+
+ @SneakyThrows
+ private String loadCertificate() {
+ var cert = Files.readString(Path.of(requireNonNull(this.getClass().getResource("/cert.pem")).toURI()));
+ return URLEncoder.encode(cert, Charset.defaultCharset());
}
}
diff --git a/src/test/java/io/gravitee/policy/sslenforcement/configuration/SslEnforcementPolicyConfigurationTest.java b/src/test/java/io/gravitee/policy/sslenforcement/configuration/SslEnforcementPolicyConfigurationTest.java
index 927ef04e..44f88dbe 100644
--- a/src/test/java/io/gravitee/policy/sslenforcement/configuration/SslEnforcementPolicyConfigurationTest.java
+++ b/src/test/java/io/gravitee/policy/sslenforcement/configuration/SslEnforcementPolicyConfigurationTest.java
@@ -15,15 +15,15 @@
*/
package io.gravitee.policy.sslenforcement.configuration;
+import static java.util.Objects.requireNonNull;
import static org.assertj.core.api.Assertions.assertThat;
import io.gravitee.json.validation.InvalidJsonException;
import io.gravitee.json.validation.JsonSchemaValidator;
import io.gravitee.json.validation.JsonSchemaValidatorImpl;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.StringWriter;
-import org.apache.commons.io.IOUtils;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import lombok.SneakyThrows;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.jupiter.api.Assertions;
@@ -36,8 +36,6 @@ class SslEnforcementPolicyConfigurationTest {
final String schema = loadResource("/schemas/schema-form.json");
- SslEnforcementPolicyConfigurationTest() throws IOException {}
-
@Test
@DisplayName("Should use default values when configuration is empty")
void shouldSetDefaultValues() {
@@ -46,6 +44,8 @@ void shouldSetDefaultValues() {
JSONObject defaultConfig = new JSONObject();
defaultConfig.put("requiresSsl", true);
defaultConfig.put("requiresClientAuthentication", false);
+ defaultConfig.put("certificateLocation", "SESSION");
+ defaultConfig.put("certificateHeaderName", "ssl-client-cert");
assertThat(validated).isEqualTo(defaultConfig.toString());
}
@@ -84,18 +84,11 @@ void shouldThrowWithInvalidDistinguishedNames() {
String config = new JSONObject().put("whitelistClientCertificates", distinguishedNames).toString();
- Assertions.assertThrows(
- InvalidJsonException.class,
- () -> {
- jsonSchemaValidator.validate(schema, config);
- }
- );
+ Assertions.assertThrows(InvalidJsonException.class, () -> jsonSchemaValidator.validate(schema, config));
}
- private String loadResource(String resource) throws IOException {
- InputStream is = this.getClass().getResourceAsStream(resource);
- StringWriter sw = new StringWriter();
- IOUtils.copy(is, sw, "UTF-8");
- return sw.toString();
+ @SneakyThrows
+ private String loadResource(String resource) {
+ return Files.readString(Path.of(requireNonNull(this.getClass().getResource(resource)).toURI()));
}
}
diff --git a/src/test/resources/cert.pem b/src/test/resources/cert.pem
new file mode 100644
index 00000000..ab0ccf7c
--- /dev/null
+++ b/src/test/resources/cert.pem
@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFdTCCA12gAwIBAgIUODffn9exS+PuJT56q99vE8KM/pUwDQYJKoZIhvcNAQEL
+BQAwSjELMAkGA1UEBhMCVVMxGTAXBgNVBAoMEFN1biBNaWNyb3N5c3RlbXMxETAP
+BgNVBAsMCEphdmFTb2Z0MQ0wCwYDVQQDDAREdWtlMB4XDTI0MDIyMjEzNDEzOVoX
+DTM0MDIxOTEzNDEzOVowSjELMAkGA1UEBhMCVVMxGTAXBgNVBAoMEFN1biBNaWNy
+b3N5c3RlbXMxETAPBgNVBAsMCEphdmFTb2Z0MQ0wCwYDVQQDDAREdWtlMIICIjAN
+BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp0TNN76qDUvDbSRezyF4TkHbuB6u
+looXpz/FdVqD95E32cE8j8nQc7iVhoiPBo4HEvIDKe91csFtQMyQnogxQGLbu+02
+Cy8hm0iUpPsRvlwiNr1nLBIR3L6P6oP1RqjzWvGDUVS6SHDfyE2dWwYTpDymqUjl
+bSvitrK5n4ED+eypgUuBaGLJQFKvxNQyC2c8pI9Z+E+VD7wrkRxtQRoPz7eLFhSe
+52jBveqTm9l9VRmpcLo0rZCHQ1PhwfuAiF/QzCRmz89T3ap3EusL00LnbeMVRM/H
+pZPR5SXhtrtJejCCt7wjHI6n62Zn6nu2P+klUeqO4fFZuK+L79dBWUsPwuAljL79
+Wt1Osvybyd842WcYiU/3b3vXkoEKCqN0Mgm6TL7lcokmCaXntztpO5o4RMuB/9xU
+FTujnQpLZCPY+V8iTTRcVpKGfk0BHuWRGW1y1lh496Bp6mCivh7uY9N6N+m5LaJe
+EZXb6oVwIxn+t7q6wLneBDqOS0+ELZaHLdV5LTVenQS8e1uVLYAkMAxkKQHgbot8
+w8c2/0iQf1fNQvrEc9Lm9+RW4fzvHujg6Ww3UW/SX5ddxlLPKPjo0z2kTUrJGB/Z
+4WmkcMJOQxu+t+CGD6ZfhgmPEUUzsEBuJvMQBrtnmXUzOFhtgVaOs9Edv+Z3XHzB
+a3YEEgff6m2ltckCAwEAAaNTMFEwHQYDVR0OBBYEFFTzVB3Kfe7gKIrkCA8nFNbm
+a+psMB8GA1UdIwQYMBaAFFTzVB3Kfe7gKIrkCA8nFNbma+psMA8GA1UdEwEB/wQF
+MAMBAf8wDQYJKoZIhvcNAQELBQADggIBAAzZwDYDIJJ47marA3FjI/z3EEo/cGIP
+kNz83CpCtCSmYS/iqFzXVpmYv6isESycQDaHj7FRerSHBIWgOdlQB+eaIpO0lhzk
+9VpWao4vqVXMWEOqQ85A7Ojz2hgtXy0mkpx0441QM6uciuVUQ13fHw4w7tUYrF+T
+nLChB5VkR+XnskX1ACQrReAO86F05k8eHLSBVlc2s+k7o+BWy+hVTquQ/38ci+nH
+Xwr/Qkz1bl6UGw1vsqlRwhyUOfUR3g3kxQe+JggykescSsEYkTGR663CGNpDnLMs
+55Tkgo/kFn/HLOp4++XgckL1GHdTkVkGh5NpstrOnktgf2lftCZEWU0v9k6suyUq
+RiQdKfXmSzCIKl+nNYpcPEGgA9jTaa5H7kKEQfvO5fP5D1cj83qDZawqsynaN3TS
+2UralembbISPQZI9hwGzt/fOFSKAgKy6oDUl4MivmmKl7aGU/i7ZcT9N2FLsHZMI
+H6ml7A08wHRfKWnYorrZ6razk8x33slwHjdMNZ4YcPgoTYYQ0atvcMWZJQQMjqHk
+fEA+BG+8v9wqSX3WD2H/V075kf4W8LzY0D6N0kkN2lozL6oywKCXbQhfAj/j5zOG
+RMKl9ZPdqf3gT1/SXQWVJzvu2gYZjo3TksmupEYBDnHe28gqdSzrL9n7QRS3pNZJ
+sh3xabNkG+BC
+-----END CERTIFICATE-----
diff --git a/src/test/resources/generate-certificates.sh b/src/test/resources/generate-certificates.sh
new file mode 100755
index 00000000..4b65f81d
--- /dev/null
+++ b/src/test/resources/generate-certificates.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+#set -e
+
+openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 3650 -nodes -subj "/C=US/O=Sun Microsystems/OU=JavaSoft/CN=Duke"
diff --git a/src/test/resources/key.pem b/src/test/resources/key.pem
new file mode 100644
index 00000000..e361c6c6
--- /dev/null
+++ b/src/test/resources/key.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+x4FCbf7oVOYtQUCOdEiJr/X8xFXSLA==
+-----END PRIVATE KEY-----