From 91d9d1bf16443a1f363ac9af2d86962a3fd55b6c Mon Sep 17 00:00:00 2001 From: Rudy De Busscher Date: Wed, 1 Sep 2021 16:51:37 +0200 Subject: [PATCH 1/2] FISH-5646: Client Certificate Validator API --- .../client/ClientCertificateValidator.java | 62 ++++++++++ nucleus/security/core/pom.xml | 7 +- .../realm/certificate/CertificateRealm.java | 109 +++++++++++++++--- 3 files changed, 158 insertions(+), 20 deletions(-) create mode 100644 api/payara-api/src/main/java/fish/payara/security/client/ClientCertificateValidator.java diff --git a/api/payara-api/src/main/java/fish/payara/security/client/ClientCertificateValidator.java b/api/payara-api/src/main/java/fish/payara/security/client/ClientCertificateValidator.java new file mode 100644 index 00000000000..154a22b9a7c --- /dev/null +++ b/api/payara-api/src/main/java/fish/payara/security/client/ClientCertificateValidator.java @@ -0,0 +1,62 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) [2021] Payara Foundation and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://github.com/payara/Payara/blob/master/LICENSE.txt + * See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * The Payara Foundation designates this particular file as subject to the "Classpath" + * exception as provided by the Payara Foundation in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package fish.payara.security.client; + +import javax.security.auth.Subject; +import javax.security.auth.x500.X500Principal; +import java.security.cert.X509Certificate; + +/** + * The Client Certificate Validator API that can be used to perform additional checks on the presented + * Client certificate. When specified, this method is executed for each request so make sure the implementation + * considers some kind of caching for checks that take some considerable time. + */ +@FunctionalInterface +public interface ClientCertificateValidator { + + /** + * @param subject The Subject object for the authentication request. + * @param principal The Principal object from the user certificate. + * @param certificate The user certificate that was presented in the request. + * @return true when the Client Certificate Validation was successful and request can continue. + * In the case of false, a {@code LoginException} will be thrown. + */ + boolean isValid(Subject subject, X500Principal principal, X509Certificate certificate); +} diff --git a/nucleus/security/core/pom.xml b/nucleus/security/core/pom.xml index fa556a8c982..d8b5b775ca1 100644 --- a/nucleus/security/core/pom.xml +++ b/nucleus/security/core/pom.xml @@ -39,7 +39,7 @@ only if the new code is made subject to such option by the copyright holder. - Portions Copyright [2019] [Payara Foundation and/or its affiliates] + Portions Copyright [2019-2021] [Payara Foundation and/or its affiliates] --> @@ -207,6 +207,11 @@ glassfish-api ${project.version} + + fish.payara.api + payara-api + provided + fish.payara.server.internal.common simple-glassfish-api diff --git a/nucleus/security/core/src/main/java/com/sun/enterprise/security/auth/realm/certificate/CertificateRealm.java b/nucleus/security/core/src/main/java/com/sun/enterprise/security/auth/realm/certificate/CertificateRealm.java index 0ef6f4a3536..4695302c69f 100644 --- a/nucleus/security/core/src/main/java/com/sun/enterprise/security/auth/realm/certificate/CertificateRealm.java +++ b/nucleus/security/core/src/main/java/com/sun/enterprise/security/auth/realm/certificate/CertificateRealm.java @@ -37,18 +37,19 @@ * only if the new code is made subject to such option by the copyright * holder. */ -// Portions Copyright [2018-2019] [Payara Foundation and/or its affiliates] +// Portions Copyright [2018-2021] [Payara Foundation and/or its affiliates] package com.sun.enterprise.security.auth.realm.certificate; -import java.security.Principal; -import java.util.Arrays; -import java.util.Collections; -import java.util.Enumeration; -import java.util.Properties; -import java.util.Set; -import java.util.function.Function; -import java.util.logging.Level; -import java.util.stream.Collectors; +import com.sun.enterprise.security.BaseRealm; +import com.sun.enterprise.security.SecurityContext; +import com.sun.enterprise.security.auth.login.DistinguishedPrincipalCredential; +import com.sun.enterprise.security.auth.login.common.LoginException; +import com.sun.enterprise.security.auth.realm.BadRealmException; +import com.sun.enterprise.security.auth.realm.NoSuchRealmException; +import com.sun.enterprise.util.Utility; +import fish.payara.security.client.ClientCertificateValidator; +import org.glassfish.security.common.Group; +import org.jvnet.hk2.annotations.Service; import javax.naming.InvalidNameException; import javax.naming.ldap.LdapName; @@ -56,15 +57,15 @@ import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.x500.X500Principal; - -import org.glassfish.security.common.Group; -import org.jvnet.hk2.annotations.Service; - -import com.sun.enterprise.security.BaseRealm; -import com.sun.enterprise.security.SecurityContext; -import com.sun.enterprise.security.auth.login.DistinguishedPrincipalCredential; -import com.sun.enterprise.security.auth.realm.BadRealmException; -import com.sun.enterprise.security.auth.realm.NoSuchRealmException; +import java.lang.ref.WeakReference; +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; /** * Realm wrapper for supporting certificate authentication. @@ -103,6 +104,12 @@ public final class CertificateRealm extends BaseRealm { /** Descriptive string of the authentication type of this realm. */ public static final String AUTH_TYPE = "certificate"; + private final Map>> clientCertificateValidatorMap + = Collections.synchronizedMap(new WeakHashMap<>()); + @Override protected void init(Properties props) throws BadRealmException, NoSuchRealmException { super.init(props); @@ -149,6 +156,8 @@ public Enumeration getGroupNames(String username) { * @return principal's name */ public String authenticate(Subject subject, X500Principal principal) { + validateSubjectViaAPI(subject, principal); + _logger.finest(() -> String.format("authenticate(subject=%s, principal=%s)", subject, principal)); final LdapName dn = getLdapName(principal); @@ -175,6 +184,68 @@ public String authenticate(Subject subject, X500Principal principal) { return principalName; } + private void validateSubjectViaAPI(Subject subject, X500Principal principal) { + X509Certificate certificate = getCertificateFromSubject(subject, principal); + if (certificate == null) { + _logger.warning(() -> String.format("No X509Certificate found(subject=%s, principal=%s)", subject, principal)); + return; + } + + // Ask all the Client Certificate Validator. + List validators = Collections.emptyList(); + try { + validators = loadValidatorClasses(); + } catch (Throwable exc) { + _logger.log(Level.WARNING, "Exception while loading certificate validation class", exc); + clientCertificateValidatorMap.remove(Utility.getClassLoader()); + } + + boolean failed = false; + if (!validators.isEmpty()) { + for (ClientCertificateValidator validator : validators) { + if (!validator.isValid(subject, principal, certificate)) { + failed = true; + } + } + } + if (failed) { + throw new LoginException("Certificate Validation Failed via API"); + } + } + + private List loadValidatorClasses() { + AtomicReference> serviceLoader = new AtomicReference<>(); + clientCertificateValidatorMap.compute(Utility.getClassLoader(), (cl, weak) -> { + serviceLoader.set(weak != null ? weak.get() : null); + if (serviceLoader.get() == null) { + serviceLoader.set(ServiceLoader.load(ClientCertificateValidator.class)); + return new WeakReference<>(serviceLoader.get()); + } else { + return weak; + } + }); + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(serviceLoader.get().iterator(), + Spliterator.ORDERED), false).collect(Collectors.toList()); + } + + private X509Certificate getCertificateFromSubject(Subject subject, X500Principal principal) { + X509Certificate result = null; + Set publicCredentials = subject.getPublicCredentials(); + for (Object publicCredential : publicCredentials) { + if (publicCredential instanceof List) { + List data = (List) publicCredential; + for (Object item : data) { + if (item instanceof X509Certificate) { + X509Certificate certificate = (X509Certificate) item; + if (principal.equals(certificate.getIssuerX500Principal())) { + result = (X509Certificate) item; + } + } + } + } + } + return result; + } private LdapName getLdapName(final X500Principal principal) { try { From 7e73ca597af46ed99827371d1c9def64aea826c6 Mon Sep 17 00:00:00 2001 From: Rudy De Busscher Date: Thu, 2 Sep 2021 08:57:03 +0200 Subject: [PATCH 2/2] FISH-5646: Review comments --- .../security/auth/realm/certificate/CertificateRealm.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nucleus/security/core/src/main/java/com/sun/enterprise/security/auth/realm/certificate/CertificateRealm.java b/nucleus/security/core/src/main/java/com/sun/enterprise/security/auth/realm/certificate/CertificateRealm.java index 4695302c69f..adea359cad7 100644 --- a/nucleus/security/core/src/main/java/com/sun/enterprise/security/auth/realm/certificate/CertificateRealm.java +++ b/nucleus/security/core/src/main/java/com/sun/enterprise/security/auth/realm/certificate/CertificateRealm.java @@ -204,7 +204,10 @@ private void validateSubjectViaAPI(Subject subject, X500Principal principal) { if (!validators.isEmpty()) { for (ClientCertificateValidator validator : validators) { if (!validator.isValid(subject, principal, certificate)) { + _logger.info(() -> String.format("Client Certificate validation failed for (subject=%s, principal=%s) by %s" + , subject, principal, validator.getClass().getName())); failed = true; + break; } } }