Skip to content

Commit

Permalink
feat: add option to load certificate from header (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
jgiovaresco authored Feb 23, 2024
1 parent 5f6c405 commit 7a2ca7b
Show file tree
Hide file tree
Showing 11 changed files with 451 additions and 241 deletions.
9 changes: 0 additions & 9 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
<bcpkix-jdk15on.version>1.70</bcpkix-jdk15on.version>

<gravitee-json-validation.version>1.0.1</gravitee-json-validation.version>
<commons-io.version>1.3.2</commons-io.version>

<maven-assembly-plugin.version>3.6.0</maven-assembly-plugin.version>
<prettier-maven-plugin.version>0.19</prettier-maven-plugin.version>
Expand Down Expand Up @@ -145,14 +144,6 @@
<version>${gravitee-json-validation.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
<scope>test</scope>
</dependency>

</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -87,15 +88,15 @@ 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;

for (String name : configuration.getWhitelistClientCertificates()) {
// 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;
Expand All @@ -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.<String, Object>builder().put("name", peerPrincipal.getName()).build()
Maps.<String, Object>builder().put("name", principal.getName()).build()
)
);

Expand All @@ -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<X509Certificate> extractCertificate(final HttpHeaders httpHeaders, final String certHeader) {
Optional<X509Certificate> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit 7a2ca7b

Please sign in to comment.