This is an automated email from the ASF dual-hosted git repository.

frankgh pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-sidecar.git


The following commit(s) were added to refs/heads/trunk by this push:
     new f2e1713b CASSANDRASC-146 Add vertx auth subproject for mTLS 
authentication (#137)
f2e1713b is described below

commit f2e1713b4249d6ab13dc27d53ea9c43950950d6e
Author: Saranya Krishnakumar <sarany...@apple.com>
AuthorDate: Wed Oct 16 19:57:33 2024 -0700

    CASSANDRASC-146 Add vertx auth subproject for mTLS authentication (#137)
    
    Co-authored-by: Saranya Krishnakumar <sarany...@apple.com>
    Co-authored-by: Raymond Welgosh <raymond.welg...@gmail.com>
    
    Patch by Saranya Krishnakumar, Raymond Welgosh; Reviewed by Francisco 
Guerrero, Yifan Cai for CASSANDRASC-146
---
 CHANGES.txt                                        |   1 +
 settings.gradle                                    |   1 +
 vertx-auth-mtls/build.gradle                       |  81 +++++++
 .../authentication/CertificateCredentials.java     | 115 +++++++++
 .../auth/mtls/CertificateIdentityExtractor.java    |  47 ++++
 .../vertx/ext/auth/mtls/CertificateValidator.java  |  46 ++++
 .../ext/auth/mtls/MutualTlsAuthentication.java     |  43 ++++
 .../mtls/impl/AllowAllCertificateValidator.java    |  41 ++++
 .../auth/mtls/impl/CertificateValidatorImpl.java   | 195 +++++++++++++++
 .../mtls/impl/MutualTlsAuthenticationImpl.java     |  79 +++++++
 .../auth/mtls/impl/SpiffeIdentityExtractor.java    | 123 ++++++++++
 .../authentication/CertificateCredentialsTest.java |  92 ++++++++
 .../mtls/impl/CertificateValidatorImplTest.java    | 105 +++++++++
 .../mtls/impl/MutualTlsAuthenticationTest.java     | 261 +++++++++++++++++++++
 .../mtls/impl/SpiffeIdentityExtractorTest.java     | 112 +++++++++
 .../ext/auth/mtls/utils/CertificateBuilder.java    | 116 +++++++++
 16 files changed, 1458 insertions(+)

diff --git a/CHANGES.txt b/CHANGES.txt
index 6079f284..1b635ef1 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
 1.0.0
 -----
+ * Add vert.x auth subproject for mTLS authentication support in Sidecar 
(CASSANDRASC-146)
  * Improve S3 download throttling with range-GetObject (CASSANDRASC-142)
  * Updating traffic shaping options throws IllegalStateException 
(CASSANDRASC-140)
  * Add restore job progress endpoint and consistency check on restore ranges 
(CASSANDRASC-132)
diff --git a/settings.gradle b/settings.gradle
index 12e28049..475191a7 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -28,4 +28,5 @@ include "server"
 include "server-common"
 include "vertx-client"
 include "vertx-client-shaded"
+include "vertx-auth-mtls"
 
diff --git a/vertx-auth-mtls/build.gradle b/vertx-auth-mtls/build.gradle
new file mode 100644
index 00000000..991fc2dc
--- /dev/null
+++ b/vertx-auth-mtls/build.gradle
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import java.nio.file.Paths
+
+plugins {
+    id('java-library')
+    id('idea')
+    id('maven-publish')
+    id('java-test-fixtures')
+}
+
+group 'org.apache.cassandra.sidecar'
+version project.version
+
+sourceCompatibility = 1.8
+
+test {
+    useJUnitPlatform()
+    maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
+    reports {
+        junitXml.enabled = true
+        def destDir = Paths.get(rootProject.rootDir.absolutePath, "build", 
"test-results", "vertx-auth-mtls").toFile()
+        println("Destination directory for vertx-auth-mtls tests: ${destDir}")
+        junitXml.destination = destDir
+        html.enabled = true
+    }
+}
+
+configurations {
+    all*.exclude(group: 'ch.qos.logback')
+}
+
+dependencies {
+    // We keep Vert.x version latest here, for easier contribution back to 
Vert.x project
+    implementation(group: 'io.vertx', name: 'vertx-auth-common', version: 
"$vertxVersion")
+    implementation(group: 'io.vertx', name: 'vertx-junit5', version: 
"$vertxVersion")
+    implementation(group: 'io.vertx', name: 'vertx-codegen', version: 
"$vertxVersion")
+    testImplementation(group: 'org.assertj', name: 'assertj-core', version: 
'3.26.3')
+    testImplementation(group: 'org.mockito', name: 'mockito-core', version: 
'4.11.0')
+    testFixturesApi(group: 'org.bouncycastle', name: 'bcpkix-jdk18on', 
version: '1.78.1')
+}
+
+java {
+    withJavadocJar()
+    withSourcesJar()
+}
+
+publishing {
+    publications {
+        maven(MavenPublication) {
+            from components.java
+            groupId project.group
+            artifactId "${archivesBaseName}"
+            version System.getenv("CODE_VERSION") ?: "${version}"
+        }
+    }
+}
+
+javadoc {
+    if (JavaVersion.current().isJava9Compatible()) {
+        options.addBooleanOption('html5', true)
+    }
+}
+
+check.dependsOn(checkstyleMain, checkstyleTest, jacocoTestReport)
diff --git 
a/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/authentication/CertificateCredentials.java
 
b/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/authentication/CertificateCredentials.java
new file mode 100644
index 00000000..15e5e9c9
--- /dev/null
+++ 
b/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/authentication/CertificateCredentials.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.vertx.ext.auth.authentication;
+
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.List;
+
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.core.json.JsonObject;
+
+/**
+ * Certificates based {@link Credentials} implementation, carries user's 
certificates, which can be used for
+ * authenticating or authorizing users.
+ */
+public class CertificateCredentials implements Credentials
+{
+    private final List<Certificate> certificateChain;
+    private final X509Certificate peerCertificate;
+
+    public CertificateCredentials(Certificate certificate)
+    {
+        this(Collections.singletonList(certificate));
+    }
+
+    public CertificateCredentials(List<Certificate> certificateChain)
+    {
+        this.certificateChain = Collections.unmodifiableList(certificateChain);
+        this.peerCertificate = getPeerCertificate();
+    }
+
+    /**
+     * Create {@link CertificateCredentials} from {@link HttpServerRequest}
+     *
+     * @return CertificateCredentials
+     */
+    public static CertificateCredentials fromHttpRequest(HttpServerRequest 
request)
+    {
+        try
+        {
+            return new 
CertificateCredentials(request.connection().peerCertificates());
+        }
+        catch (Exception e)
+        {
+            throw new IllegalArgumentException("Could not extract certificates 
from request", e);
+        }
+    }
+
+    /**
+     * @return The certificate chain contained in {@link 
CertificateCredentials}
+     */
+    public List<Certificate> certificateChain()
+    {
+        return certificateChain;
+    }
+
+    /**
+     * @return peer's certificate. It does not return null value once {@link 
#checkValid()} passes
+     */
+    public X509Certificate peerCertificate()
+    {
+        return peerCertificate;
+    }
+
+    public void checkValid() throws CredentialValidationException
+    {
+        checkValid(this);
+    }
+
+    @Override
+    public <V> void checkValid(V arg) throws CredentialValidationException
+    {
+        if (certificateChain.isEmpty())
+        {
+            throw new CredentialValidationException("Certificate Chain cannot 
be empty");
+        }
+    }
+
+    /**
+     * @deprecated {@link CertificateCredentials} currently not represented in 
Json format.
+     */
+    @Override
+    public JsonObject toJson()
+    {
+        throw new UnsupportedOperationException("Deprecated authentication 
method");
+    }
+
+    private X509Certificate getPeerCertificate()
+    {
+        // First certificate in the chain is peer's own cert
+        if (!certificateChain.isEmpty() && certificateChain.get(0) instanceof 
X509Certificate)
+        {
+            return (X509Certificate) certificateChain.get(0);
+        }
+
+        return null;
+    }
+}
diff --git 
a/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/CertificateIdentityExtractor.java
 
b/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/CertificateIdentityExtractor.java
new file mode 100644
index 00000000..f2cf8994
--- /dev/null
+++ 
b/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/CertificateIdentityExtractor.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.vertx.ext.auth.mtls;
+
+import io.vertx.ext.auth.authentication.CertificateCredentials;
+import io.vertx.ext.auth.authentication.CredentialValidationException;
+
+/**
+ * {@link CertificateIdentityExtractor} extracts a valid identity from 
certificate chain. Interface can be extended to
+ * implement custom certificate identity validators.
+ */
+public interface CertificateIdentityExtractor
+{
+    /**
+     * Extracts a valid identity out of {@link CertificateCredentials} 
certificate chain. This identity can later be used
+     * for authorizing user's resource level permissions. If a valid identity 
could not be extracted, then throws
+     * {@code CredentialValidationException}
+     *
+     * <p>An example of identity could be the following:
+     * <ul>
+     *  <li>an identifier in SAN of the certificate like SPIFFE
+     *  <li>CN of the certificate
+     *  <li>any other fields in the certificate can be combined and be used as 
identifier of the certificate
+     * </ul>
+     *
+     * @param certificateCredentials certificate chain of user that is already 
verified
+     * @return {@code String} identity string extracted from certificate, 
uniquely represents client
+     * @throws CredentialValidationException when a valid identity cannot be 
extracted from certificate chain.
+     */
+    String validIdentity(CertificateCredentials certificateCredentials) throws 
CredentialValidationException;
+}
diff --git 
a/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/CertificateValidator.java
 
b/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/CertificateValidator.java
new file mode 100644
index 00000000..ef9c7963
--- /dev/null
+++ 
b/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/CertificateValidator.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.vertx.ext.auth.mtls;
+
+import io.vertx.ext.auth.authentication.CertificateCredentials;
+import io.vertx.ext.auth.authentication.CredentialValidationException;
+
+/**
+ * Interface for validating certificates for mutual TLS authentication.
+ * <p>
+ * This interface can be implemented to provide logic for validating various 
fields from Certificates.
+ */
+public interface CertificateValidator
+{
+    /**
+     * Validates if certificates shared as part of {@link 
CertificateCredentials} are valid.
+     *
+     * <p>For example:
+     * <ul>
+     *  <li>Verifying CA information
+     *  <li>Checking CN information
+     *  <li>Validating Issuer information
+     *  <li>Checking organization information etc
+     * </ul>
+     *
+     * @param credentials user certificate credentials shared
+     * @throws CredentialValidationException when certificate is not valid.
+     */
+    void verifyCertificate(CertificateCredentials credentials) throws 
CredentialValidationException;
+}
diff --git 
a/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/MutualTlsAuthentication.java
 
b/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/MutualTlsAuthentication.java
new file mode 100644
index 00000000..c54f9d4a
--- /dev/null
+++ 
b/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/MutualTlsAuthentication.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.vertx.ext.auth.mtls;
+
+import io.vertx.codegen.annotations.VertxGen;
+import io.vertx.ext.auth.authentication.AuthenticationProvider;
+import io.vertx.ext.auth.mtls.impl.MutualTlsAuthenticationImpl;
+
+/**
+ * Factory interface for creating a MTLS {@link 
io.vertx.ext.auth.authentication.AuthenticationProvider}.
+ */
+@VertxGen
+public interface MutualTlsAuthentication extends AuthenticationProvider
+{
+    /**
+     * Create a MTLS authentication provider
+     *
+     * @param certificateValidator {@link CertificateValidator} for validating 
details within {@link io.vertx.ext.auth.authentication.CertificateCredentials}
+     * @param identityExtractor    {@link CertificateIdentityExtractor} for 
extracting valid identity out of {@link 
io.vertx.ext.auth.authentication.CertificateCredentials}
+     * @return the authentication provider
+     */
+    static MutualTlsAuthentication create(CertificateValidator 
certificateValidator,
+                                          CertificateIdentityExtractor 
identityExtractor)
+    {
+        return new MutualTlsAuthenticationImpl(certificateValidator, 
identityExtractor);
+    }
+}
diff --git 
a/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/impl/AllowAllCertificateValidator.java
 
b/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/impl/AllowAllCertificateValidator.java
new file mode 100644
index 00000000..ccf23bf3
--- /dev/null
+++ 
b/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/impl/AllowAllCertificateValidator.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.vertx.ext.auth.mtls.impl;
+
+import io.vertx.ext.auth.authentication.CertificateCredentials;
+import io.vertx.ext.auth.mtls.CertificateValidator;
+
+/**
+ * {@link AllowAllCertificateValidator} can be used when certificate specific 
details are not required to be
+ * validated. Use {@link CertificateValidatorImpl} if specific details like 
CN, issuer organization
+ * are to be verified.
+ */
+public class AllowAllCertificateValidator implements CertificateValidator
+{
+    /**
+     * Marks all shared {@link CertificateCredentials} as valid.
+     *
+     * @param credentials client credentials shared
+     */
+    @Override
+    public void verifyCertificate(CertificateCredentials credentials)
+    {
+        // do nothing
+    }
+}
diff --git 
a/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/impl/CertificateValidatorImpl.java
 
b/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/impl/CertificateValidatorImpl.java
new file mode 100644
index 00000000..2556fe9a
--- /dev/null
+++ 
b/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/impl/CertificateValidatorImpl.java
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.vertx.ext.auth.mtls.impl;
+
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+
+import io.vertx.ext.auth.authentication.CertificateCredentials;
+import io.vertx.ext.auth.authentication.CredentialValidationException;
+import io.vertx.ext.auth.mtls.CertificateValidator;
+
+/**
+ * {@link CertificateValidator} implementation that can be used for validating 
certificates.
+ */
+public class CertificateValidatorImpl implements CertificateValidator
+{
+    private final Set<String> trustedCNs;
+    private final String trustedIssuerOrganization;
+    private final String trustedIssuerOrganizationUnit;
+    private final String trustedIssuerCountry;
+
+    public CertificateValidatorImpl(Set<String> trustedCNs,
+                                    String trustedIssuerOrganization,
+                                    String trustedIssuerOrganizationUnit,
+                                    String trustedIssuerCountry)
+    {
+        this.trustedCNs = Collections.unmodifiableSet(trustedCNs);
+        this.trustedIssuerOrganization = trustedIssuerOrganization;
+        this.trustedIssuerOrganizationUnit = trustedIssuerOrganizationUnit;
+        this.trustedIssuerCountry = trustedIssuerCountry;
+    }
+
+    @Override
+    public void verifyCertificate(CertificateCredentials credentials)
+    {
+        credentials.checkValid();
+        X509Certificate peerCertificate = credentials.peerCertificate();
+        if (peerCertificate == null)
+        {
+            throw new CredentialValidationException("No X509Certificate found 
for validating");
+        }
+        validateIssuer(peerCertificate);
+        try
+        {
+            peerCertificate.checkValidity();
+        }
+        catch (CertificateExpiredException e)
+        {
+            throw new CredentialValidationException("Expired certificates 
shared for authentication", e);
+        }
+        catch (CertificateNotYetValidException e)
+        {
+            throw new CredentialValidationException("Invalid certificates 
shared", e);
+        }
+    }
+
+    private void validateIssuer(X509Certificate certificate)
+    {
+        List<Attributes> issuerAttrs;
+        try
+        {
+            issuerAttrs = getAttributes(new 
LdapName(certificate.getIssuerDN().getName()));
+            validateCN(issuerAttrs);
+            validateAttribute(issuerAttrs, "O", trustedIssuerOrganization);
+            validateAttribute(issuerAttrs, "OU", 
trustedIssuerOrganizationUnit);
+            validateAttribute(issuerAttrs, "C", trustedIssuerCountry);
+        }
+        catch (NamingException e)
+        {
+            throw new CredentialValidationException("Expected issuer 
attributes could not be extracted", e);
+        }
+    }
+
+    private void validateCN(List<Attributes> attributes) throws NamingException
+    {
+        if (trustedCNs.isEmpty())
+        {
+            return;
+        }
+        String attribute = getAttribute(attributes, "CN");
+        if (!trustedCNs.contains(attribute))
+        {
+            throw new CredentialValidationException("CN " + attribute + " not 
trusted");
+        }
+    }
+
+    private void validateAttribute(List<Attributes> attributes, String 
attributeName, String trustedAttribute) throws NamingException
+    {
+        if (trustedAttribute == null)
+        {
+            return;
+        }
+        String attribute = getAttribute(attributes, attributeName);
+        if (!attribute.equalsIgnoreCase(trustedAttribute))
+        {
+            throw new CredentialValidationException(attribute + " attribute 
not trusted");
+        }
+    }
+
+    private List<Attributes> getAttributes(LdapName ldapName)
+    {
+        List<Rdn> rdns = ldapName.getRdns();
+        List<Attributes> attributes = new ArrayList<>(rdns.size());
+        for (int i = 0; i < rdns.size(); ++i)
+        {
+            attributes.add(rdns.get(i).toAttributes());
+        }
+        return attributes;
+    }
+
+    private String getAttribute(List<Attributes> attributesList, String 
attributeName) throws NamingException
+    {
+        for (int i = 0; i < attributesList.size(); ++i)
+        {
+            Attributes attributes = attributesList.get(i);
+            Attribute value = attributes.get(attributeName);
+            if (value != null)
+            {
+                return value.get().toString();
+            }
+        }
+        throw new CredentialValidationException(String.format("Expected 
attribute %s not found", attributeName));
+    }
+
+    public static Builder builder()
+    {
+        return new Builder();
+    }
+
+    /**
+     * Builder that provides default implementation of {@link 
CertificateValidator}.
+     */
+    public static class Builder
+    {
+        Set<String> trustedCNs = Collections.emptySet();
+        String trustedIssuerOrganization;
+        String trustedIssuerOrganizationUnit;
+        String trustedIssuerCountry;
+
+        public Builder trustedCNs(Set<String> trustedCNs)
+        {
+            this.trustedCNs = trustedCNs;
+            return this;
+        }
+
+        public Builder trustedIssuerOrganization(String 
trustedIssuerOrganization)
+        {
+            this.trustedIssuerOrganization = trustedIssuerOrganization;
+            return this;
+        }
+
+        public Builder trustedIssuerOrganizationUnit(String 
trustedIssuerOrganizationUnit)
+        {
+            this.trustedIssuerOrganizationUnit = trustedIssuerOrganizationUnit;
+            return this;
+        }
+
+        public Builder trustedIssuerCountry(String trustedIssuerCountry)
+        {
+            this.trustedIssuerCountry = trustedIssuerCountry;
+            return this;
+        }
+
+        public CertificateValidatorImpl build()
+        {
+            return new CertificateValidatorImpl(trustedCNs, 
trustedIssuerOrganization, trustedIssuerOrganizationUnit, trustedIssuerCountry);
+        }
+    }
+}
diff --git 
a/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/impl/MutualTlsAuthenticationImpl.java
 
b/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/impl/MutualTlsAuthenticationImpl.java
new file mode 100644
index 00000000..c6cef9b5
--- /dev/null
+++ 
b/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/impl/MutualTlsAuthenticationImpl.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.vertx.ext.auth.mtls.impl;
+
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.auth.User;
+import io.vertx.ext.auth.authentication.AuthenticationProvider;
+import io.vertx.ext.auth.authentication.CertificateCredentials;
+import io.vertx.ext.auth.authentication.Credentials;
+import io.vertx.ext.auth.mtls.CertificateIdentityExtractor;
+import io.vertx.ext.auth.mtls.CertificateValidator;
+import io.vertx.ext.auth.mtls.MutualTlsAuthentication;
+
+/**
+ * {@link AuthenticationProvider} implementation for mTLS (MutualTLS) 
authentication. With mTLS authentication
+ * both server and client exchange certificates and validates each other's 
certificates.
+ */
+public class MutualTlsAuthenticationImpl implements MutualTlsAuthentication
+{
+    private final CertificateValidator certificateValidator;
+    private final CertificateIdentityExtractor identityExtractor;
+
+    public MutualTlsAuthenticationImpl(CertificateValidator 
certificateValidator,
+                                       CertificateIdentityExtractor 
identityExtractor)
+    {
+        this.certificateValidator = certificateValidator;
+        this.identityExtractor = identityExtractor;
+    }
+
+    @Override
+    public Future<User> authenticate(Credentials credentials)
+    {
+        if (!(credentials instanceof CertificateCredentials))
+        {
+            return Future.failedFuture("CertificateCredentials expected for 
mTLS authentication");
+        }
+
+        CertificateCredentials certificateCredentials = 
(CertificateCredentials) credentials;
+        try
+        {
+            certificateValidator.verifyCertificate(certificateCredentials);
+            String identity = 
identityExtractor.validIdentity(certificateCredentials);
+            return Future.succeededFuture(User.fromName(identity));
+        }
+        catch (Exception e)
+        {
+            return Future.failedFuture(e);
+        }
+    }
+
+    /**
+     * Use {@code authenticate(Credentials credentials, 
Handler<AsyncResult<User>> resultHandler)} instead
+     */
+    @Deprecated
+    @Override
+    public void authenticate(JsonObject credentials, 
Handler<AsyncResult<User>> resultHandler)
+    {
+        throw new UnsupportedOperationException("Deprecated authentication 
method");
+    }
+}
diff --git 
a/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/impl/SpiffeIdentityExtractor.java
 
b/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/impl/SpiffeIdentityExtractor.java
new file mode 100644
index 00000000..27a7fc72
--- /dev/null
+++ 
b/vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/impl/SpiffeIdentityExtractor.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.vertx.ext.auth.mtls.impl;
+
+import java.security.cert.X509Certificate;
+import java.util.Collection;
+import java.util.List;
+
+import io.vertx.ext.auth.authentication.CertificateCredentials;
+import io.vertx.ext.auth.authentication.CredentialValidationException;
+import io.vertx.ext.auth.mtls.CertificateIdentityExtractor;
+
+/**
+ * {@link CertificateIdentityExtractor} implementation for SPIFFE certificates 
for extracting valid SPIFFE identity.
+ * SPIFFE is a URI, present as part of SAN of client certificates, it uniquely 
identifies client.
+ */
+public class SpiffeIdentityExtractor implements CertificateIdentityExtractor
+{
+    // SpiffeIdentityExtractor can extract and validate only URI type SAN 
identities. As per RFC 5280 standards,
+    // here https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 URI 
type is represented with number 6.
+    private static final int SUBJECT_ALT_NAME_URI_TYPE = 6;
+    private static final String SPIFFE_PREFIX = "spiffe://";
+    private final String trustedDomain;
+
+    public SpiffeIdentityExtractor()
+    {
+        this(null);
+    }
+
+    public SpiffeIdentityExtractor(String trustedDomain)
+    {
+        this.trustedDomain = trustedDomain;
+    }
+
+    @Override
+    public String validIdentity(CertificateCredentials certificateCredentials) 
throws CredentialValidationException
+    {
+        // First certificate in certificate chain is usually PrivateKeyEntry.
+        X509Certificate privateCert = certificateCredentials.peerCertificate();
+
+        if (privateCert == null)
+        {
+            throw new CredentialValidationException("No X509Certificate found 
for validating");
+        }
+
+        String identity = extractIdentity(privateCert);
+        validateIdentity(identity);
+        return identity;
+    }
+
+    protected String extractIdentity(X509Certificate certificate)
+    {
+        try
+        {
+            Collection<List<?>> subjectAltNames = 
certificate.getSubjectAlternativeNames();
+            for (List<?> item : subjectAltNames)
+            {
+                Integer type = (Integer) item.get(0);
+                String identity = (String) item.get(1);
+                if (type == SUBJECT_ALT_NAME_URI_TYPE && 
identity.startsWith(SPIFFE_PREFIX))
+                {
+                    return identity;
+                }
+            }
+        }
+        catch (Exception e)
+        {
+            throw new CredentialValidationException("Error reading SAN of 
certificate", e);
+        }
+        throw new CredentialValidationException("Unable to extract SPIFFE 
identity from certificate");
+    }
+
+    protected void validateIdentity(String identity)
+    {
+        verifyPrefix(identity);
+        if (trustedDomain != null)
+        {
+            verifyDomain(identity);
+        }
+    }
+
+    private void verifyPrefix(String identity)
+    {
+        if (!identity.startsWith(SPIFFE_PREFIX))
+        {
+            throw new CredentialValidationException("SPIFFE identity must 
start with prefix " + SPIFFE_PREFIX);
+        }
+    }
+
+    private void verifyDomain(String identity)
+    {
+        String uriSuffix = identity.replaceFirst(SPIFFE_PREFIX, "");
+        String[] uriSuffixParts = uriSuffix.split("/");
+        boolean domainPresentCheck = uriSuffixParts.length > 0;
+
+        if (!domainPresentCheck)
+        {
+            throw new CredentialValidationException("SPIFFE identity extracted 
" + identity + " does not contain domain information");
+        }
+
+        String domain = uriSuffixParts[0];
+        if (!domain.equals(trustedDomain))
+        {
+            throw new CredentialValidationException("SPIFFE Identity domain " 
+ domain + " is not trusted");
+        }
+    }
+}
diff --git 
a/vertx-auth-mtls/src/test/java/io/vertx/ext/auth/authentication/CertificateCredentialsTest.java
 
b/vertx-auth-mtls/src/test/java/io/vertx/ext/auth/authentication/CertificateCredentialsTest.java
new file mode 100644
index 00000000..75796c7a
--- /dev/null
+++ 
b/vertx-auth-mtls/src/test/java/io/vertx/ext/auth/authentication/CertificateCredentialsTest.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.vertx.ext.auth.authentication;
+
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.ext.auth.mtls.utils.CertificateBuilder;
+
+import static org.assertj.core.api.Assertions.assertThatNoException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests {@link CertificateCredentials}
+ */
+public class CertificateCredentialsTest
+{
+    @Test
+    void testValidCertificate()
+    {
+        assertThatNoException().isThrownBy(() -> 
createTestCredentials().checkValid());
+    }
+
+    @Test
+    void testEmptyCertificateChain()
+    {
+        List<Certificate> certificateChain = Collections.emptyList();
+        assertThatThrownBy(() -> new 
CertificateCredentials(certificateChain).checkValid())
+        .isInstanceOf(CredentialValidationException.class);
+    }
+
+    @Test
+    void testNonCertificateBasedConnection()
+    {
+        HttpServerRequest request = mock(HttpServerRequest.class);
+
+        assertThatThrownBy(() -> 
CertificateCredentials.fromHttpRequest(request))
+        .isInstanceOf(IllegalArgumentException.class)
+        .hasMessage("Could not extract certificates from request");
+    }
+
+    @Test
+    void testToJson()
+    {
+        Certificate certificate = mock(Certificate.class);
+        CertificateCredentials credentials = new 
CertificateCredentials(certificate);
+        assertThatThrownBy(() -> credentials.toJson())
+        .isInstanceOf(UnsupportedOperationException.class);
+    }
+
+    public static CertificateCredentials createTestCredentials()
+    {
+        return createTestCredentials("CN=Vertx Auth, OU=ssl_test, O=Vertx, 
L=Unknown, ST=Unknown, C=US");
+    }
+
+    public static CertificateCredentials createTestCredentials(String 
issuerName)
+    {
+        try
+        {
+            X509Certificate certificate = CertificateBuilder.builder()
+                                                            
.issuerName(issuerName)
+                                                            .buildSelfSigned();
+            return new 
CertificateCredentials(Collections.singletonList(certificate));
+        }
+        catch (Exception e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git 
a/vertx-auth-mtls/src/test/java/io/vertx/ext/auth/mtls/impl/CertificateValidatorImplTest.java
 
b/vertx-auth-mtls/src/test/java/io/vertx/ext/auth/mtls/impl/CertificateValidatorImplTest.java
new file mode 100644
index 00000000..38a526a3
--- /dev/null
+++ 
b/vertx-auth-mtls/src/test/java/io/vertx/ext/auth/mtls/impl/CertificateValidatorImplTest.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.vertx.ext.auth.mtls.impl;
+
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Collections;
+import java.util.Date;
+
+import org.junit.jupiter.api.Test;
+
+import io.vertx.ext.auth.authentication.CertificateCredentials;
+import io.vertx.ext.auth.authentication.CredentialValidationException;
+import io.vertx.ext.auth.mtls.CertificateValidator;
+import io.vertx.ext.auth.mtls.utils.CertificateBuilder;
+
+import static 
io.vertx.ext.auth.authentication.CertificateCredentialsTest.createTestCredentials;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests {@link io.vertx.ext.auth.mtls.impl.CertificateValidatorImpl}
+ */
+public class CertificateValidatorImplTest
+{
+    private final CertificateValidator certificateValidator = 
CertificateValidatorImpl.builder()
+                                                                               
       .trustedCNs(Collections.singleton("Vertx Auth"))
+                                                                               
       .trustedIssuerOrganization("Vertx")
+                                                                               
       .trustedIssuerOrganizationUnit("ssl_test")
+                                                                               
       .trustedIssuerCountry("US")
+                                                                               
       .build();
+
+    @Test
+    public void testValidCertificateCredentials()
+    {
+        CertificateCredentials credentials = createTestCredentials();
+        certificateValidator.verifyCertificate(credentials);
+    }
+
+    @Test
+    public void testInvalidCertificateType()
+    {
+        Certificate certificate = mock(Certificate.class);
+        CertificateCredentials credentials = new 
CertificateCredentials(Collections.singletonList(certificate));
+        assertThatThrownBy(() -> 
certificateValidator.verifyCertificate(credentials))
+        .isInstanceOf(CredentialValidationException.class)
+        .hasMessage("No X509Certificate found for validating");
+    }
+
+    @Test
+    public void testNonTrustedIssuer()
+    {
+        CertificateCredentials credentials = createTestCredentials("CN=Vertx 
Auth, OU=ssl_test, " +
+                                                                   
"O=NonTrustedOrganization, " +
+                                                                   "L=Unknown, 
ST=Unknown, C=US");
+        assertThatThrownBy(() -> 
certificateValidator.verifyCertificate(credentials))
+        .isInstanceOf(CredentialValidationException.class)
+        .hasMessage("NonTrustedOrganization attribute not trusted");
+    }
+
+    @Test
+    public void testInvalidIssuer()
+    {
+        CertificateValidator certificateValidator
+        = CertificateValidatorImpl.builder()
+                                  .trustedCNs(Collections.singleton("Vertx 
Auth"))
+                                  
.trustedIssuerOrganization("MissingIssuerOrganization").trustedIssuerOrganizationUnit("ssl_test")
+                                  .trustedIssuerCountry("US").build();
+        CertificateCredentials credentials = createTestCredentials("CN=Vertx 
Auth, OU=ssl_test, L=Unknown, ST=Unknown, C=US");
+        assertThatThrownBy(() -> 
certificateValidator.verifyCertificate(credentials))
+        .isInstanceOf(CredentialValidationException.class)
+        .hasMessage("Expected attribute O not found");
+    }
+
+    @Test
+    public void testExpiredCertificate() throws Exception
+    {
+        X509Certificate certificate
+        = CertificateBuilder.builder()
+                            .notAfter(Date.from(Instant.now().minus(1, 
ChronoUnit.DAYS)))
+                            .issuerName("CN=Vertx Auth, OU=ssl_test, O=Vertx, 
L=Unknown, ST=Unknown, C=US").buildSelfSigned();
+        CertificateCredentials credentials = new 
CertificateCredentials(Collections.singletonList(certificate));
+        assertThatThrownBy(() -> 
certificateValidator.verifyCertificate(credentials))
+        .isInstanceOf(CredentialValidationException.class)
+        .hasMessage("Expired certificates shared for authentication");
+    }
+}
diff --git 
a/vertx-auth-mtls/src/test/java/io/vertx/ext/auth/mtls/impl/MutualTlsAuthenticationTest.java
 
b/vertx-auth-mtls/src/test/java/io/vertx/ext/auth/mtls/impl/MutualTlsAuthenticationTest.java
new file mode 100644
index 00000000..fe6d9c18
--- /dev/null
+++ 
b/vertx-auth-mtls/src/test/java/io/vertx/ext/auth/mtls/impl/MutualTlsAuthenticationTest.java
@@ -0,0 +1,261 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.vertx.ext.auth.mtls.impl;
+
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import io.netty.handler.ssl.util.SelfSignedCertificate;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.auth.authentication.CertificateCredentials;
+import io.vertx.ext.auth.authentication.TokenCredentials;
+import io.vertx.ext.auth.mtls.CertificateIdentityExtractor;
+import io.vertx.ext.auth.mtls.CertificateValidator;
+import io.vertx.ext.auth.mtls.MutualTlsAuthentication;
+import io.vertx.ext.auth.mtls.utils.CertificateBuilder;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests {@link MutualTlsAuthenticationImpl}
+ */
+@ExtendWith(VertxExtension.class)
+public class MutualTlsAuthenticationTest
+{
+    private static final CertificateValidator ALLOW_ALL_CERTIFICATE_VALIDATOR 
= new AllowAllCertificateValidator();
+    MutualTlsAuthentication mTlsAuth;
+    SelfSignedCertificate validCert;
+
+    @BeforeEach
+    public void setUp() throws CertificateException
+    {
+        validCert = new SelfSignedCertificate();
+    }
+
+    @Test
+    public void testSuccess(VertxTestContext context)
+    {
+        CertificateIdentityExtractor mockIdentityExtracter = 
mock(CertificateIdentityExtractor.class);
+
+        mTlsAuth = 
MutualTlsAuthentication.create(ALLOW_ALL_CERTIFICATE_VALIDATOR, 
mockIdentityExtracter);
+        List<Certificate> certChain = 
Collections.singletonList(validCert.cert());
+        CertificateCredentials credentials = new 
CertificateCredentials(certChain);
+
+        
when(mockIdentityExtracter.validIdentity(credentials)).thenReturn("default");
+
+        mTlsAuth.authenticate(credentials)
+                .onFailure(res -> context.failNow("mTls should have 
succeeded"))
+                .onSuccess(res -> context.completeNow());
+    }
+
+    @Test
+    public void testWithTokenCredentials(VertxTestContext context)
+    {
+        CertificateValidator mockCertificateValidator = 
mock(CertificateValidator.class);
+        CertificateIdentityExtractor mockIdentityExtracter = 
mock(CertificateIdentityExtractor.class);
+
+        mTlsAuth = new MutualTlsAuthenticationImpl(mockCertificateValidator, 
mockIdentityExtracter);
+
+        TokenCredentials creds = new TokenCredentials();
+
+        mTlsAuth.authenticate(creds)
+                .onSuccess(res -> context.failNow("Should have failed"))
+                .onFailure(res -> context.verify(() -> {
+                    assertThat(res).isNotNull();
+                    
assertThat(res.getMessage()).contains("CertificateCredentials expected for mTLS 
authentication");
+                    context.completeNow();
+                }));
+    }
+
+    @Test
+    public void testValidCertificate(VertxTestContext context) throws Exception
+    {
+        CertificateValidator certificateValidator
+        = new CertificateValidatorImpl(Collections.singleton("Vertx Auth"), 
"oss", "ssl_test", "US");
+        CertificateIdentityExtractor identityExtracter = new 
SpiffeIdentityExtractor();
+
+        mTlsAuth = new MutualTlsAuthenticationImpl(certificateValidator, 
identityExtracter);
+
+        X509Certificate certificate
+        = CertificateBuilder
+          .builder()
+          .issuerName("CN=Vertx Auth, OU=ssl_test, O=oss, L=Unknown, 
ST=Unknown, C=US")
+          .addSanUriName("spiffe://vertx.auth/unitTest/mtls")
+          .buildSelfSigned();
+        List<Certificate> certChain = Collections.singletonList(certificate);
+        CertificateCredentials credentials = new 
CertificateCredentials(certChain);
+
+        mTlsAuth.authenticate(credentials)
+                .onFailure(res -> context.failNow("mTls should have 
succeeded"))
+                .onSuccess(res -> context.completeNow());
+    }
+
+    @Test
+    public void testInvalidCertificate(VertxTestContext context) throws 
Exception
+    {
+        CertificateValidator certificateValidator
+        = new CertificateValidatorImpl(Collections.singleton("Vertx Auth"), 
"oss", "ssl_test", "US");
+        CertificateIdentityExtractor identityExtracter = new 
SpiffeIdentityExtractor();
+
+        mTlsAuth = new MutualTlsAuthenticationImpl(certificateValidator, 
identityExtracter);
+
+        Date yesterday = Date.from(Instant.now().minus(1, ChronoUnit.DAYS));
+        X509Certificate certificate
+        = CertificateBuilder
+          .builder()
+          .issuerName("CN=Vertx Auth, OU=ssl_test, O=oss, L=Unknown, 
ST=Unknown, C=US")
+          .addSanUriName("spiffe://vertx.auth/unitTest/mtls")
+          .notAfter(yesterday)
+          .buildSelfSigned();
+        List<Certificate> certChain = Collections.singletonList(certificate);
+        CertificateCredentials credentials = new 
CertificateCredentials(certChain);
+
+        mTlsAuth.authenticate(credentials)
+                .onSuccess(res -> context.failNow("Should have failed"))
+                .onFailure(res -> {
+                    assertThat(res).isNotNull();
+                    assertThat(res.getMessage()).contains("Expired 
certificates shared for authentication");
+                    context.completeNow();
+                });
+    }
+
+    @Test
+    public void 
testUnknownExceptionInCertertificateValidation(VertxTestContext context)
+    {
+        CertificateValidator mockCertificateValidator = 
mock(CertificateValidator.class);
+        CertificateIdentityExtractor mockIdentityExtracter = 
mock(CertificateIdentityExtractor.class);
+
+        mTlsAuth = new MutualTlsAuthenticationImpl(mockCertificateValidator, 
mockIdentityExtracter);
+        Certificate mockCertificate = mock(Certificate.class);
+        CertificateCredentials credentials = new 
CertificateCredentials(Collections.singletonList(mockCertificate));
+
+        doThrow(new RuntimeException("Invalid 
certificate")).when(mockCertificateValidator).verifyCertificate(credentials);
+
+        mTlsAuth.authenticate(credentials)
+                .onSuccess(res -> context.failNow("Should have failed"))
+                .onFailure(res -> {
+                    assertThat(res).isNotNull();
+                    assertThat(res.getMessage()).contains("Invalid 
certificate");
+                    context.completeNow();
+                });
+    }
+
+    @Test
+    public void testUnknownExceptionInIdentityExtraction(VertxTestContext 
context)
+    {
+        CertificateIdentityExtractor mockIdentityExtracter = 
mock(CertificateIdentityExtractor.class);
+
+        mTlsAuth = new 
MutualTlsAuthenticationImpl(ALLOW_ALL_CERTIFICATE_VALIDATOR, 
mockIdentityExtracter);
+        List<Certificate> certChain = 
Collections.singletonList(validCert.cert());
+        CertificateCredentials credentials = new 
CertificateCredentials(certChain);
+
+        when(mockIdentityExtracter.validIdentity(credentials)).thenThrow(new 
RuntimeException("Bad Identity"));
+
+        mTlsAuth.authenticate(credentials)
+                .onSuccess(res -> context.failNow("Should have failed"))
+                .onFailure(res -> {
+                    assertThat(res).isNotNull();
+                    assertThat(res.getMessage()).contains("Bad Identity");
+                    context.completeNow();
+                });
+    }
+
+    @Test
+    public void testEmptyIdentity(VertxTestContext context) throws Exception
+    {
+        CertificateValidator certificateValidator
+        = new CertificateValidatorImpl(Collections.singleton("Vertx Auth"), 
"oss", "ssl_test", "US");
+        CertificateIdentityExtractor identityExtracter = new 
SpiffeIdentityExtractor();
+
+        mTlsAuth = new MutualTlsAuthenticationImpl(certificateValidator, 
identityExtracter);
+
+        X509Certificate certificate
+        = CertificateBuilder
+          .builder()
+          .issuerName("CN=Vertx Auth, OU=ssl_test, O=oss, L=Unknown, 
ST=Unknown, C=US")
+          .addSanUriName("")
+          .buildSelfSigned();
+        List<Certificate> certChain = Collections.singletonList(certificate);
+        CertificateCredentials credentials = new 
CertificateCredentials(certChain);
+
+        mTlsAuth.authenticate(credentials)
+                .onSuccess(res -> context.failNow("Should have failed"))
+                .onFailure(res -> {
+                    assertThat(res).isNotNull();
+                    assertThat(res.getMessage()).contains("Error reading SAN 
of certificate");
+                    context.completeNow();
+                });
+    }
+
+    @Test
+    public void testInvalidIdentity(VertxTestContext context) throws Exception
+    {
+        CertificateValidator certificateValidator
+        = new CertificateValidatorImpl(Collections.singleton("Vertx Auth"), 
"oss", "ssl_test", "US");
+        CertificateIdentityExtractor identityExtracter = new 
SpiffeIdentityExtractor();
+
+        mTlsAuth = new MutualTlsAuthenticationImpl(certificateValidator, 
identityExtracter);
+
+        X509Certificate certificate
+        = CertificateBuilder
+          .builder()
+          .issuerName("CN=Vertx Auth, OU=ssl_test, O=oss, L=Unknown, 
ST=Unknown, C=US")
+          .addSanUriName("badIdentity")
+          .buildSelfSigned();
+        List<Certificate> certChain = Collections.singletonList(certificate);
+        CertificateCredentials credentials = new 
CertificateCredentials(certChain);
+
+        mTlsAuth.authenticate(credentials)
+                .onSuccess(res -> context.failNow("Should have failed"))
+                .onFailure(res -> context.verify(() -> {
+                    assertThat(res).isNotNull();
+                    assertThat(res.getMessage()).contains("Error reading SAN 
of certificate");
+                    context.completeNow();
+                }));
+    }
+
+    @Test
+    public void testAuthenticateWithJson()
+    {
+        CertificateIdentityExtractor mockIdentityExtracter = 
mock(CertificateIdentityExtractor.class);
+
+        mTlsAuth = new 
MutualTlsAuthenticationImpl(ALLOW_ALL_CERTIFICATE_VALIDATOR, 
mockIdentityExtracter);
+        JsonObject json = new JsonObject();
+
+        assertThatThrownBy(() -> mTlsAuth.authenticate(json, user -> {
+        }))
+        .isInstanceOf(UnsupportedOperationException.class);
+    }
+}
diff --git 
a/vertx-auth-mtls/src/test/java/io/vertx/ext/auth/mtls/impl/SpiffeIdentityExtractorTest.java
 
b/vertx-auth-mtls/src/test/java/io/vertx/ext/auth/mtls/impl/SpiffeIdentityExtractorTest.java
new file mode 100644
index 00000000..e9bfa87a
--- /dev/null
+++ 
b/vertx-auth-mtls/src/test/java/io/vertx/ext/auth/mtls/impl/SpiffeIdentityExtractorTest.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.vertx.ext.auth.mtls.impl;
+
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+
+import org.junit.jupiter.api.Test;
+
+import io.vertx.ext.auth.authentication.CertificateCredentials;
+import io.vertx.ext.auth.authentication.CredentialValidationException;
+import io.vertx.ext.auth.mtls.utils.CertificateBuilder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests {@link io.vertx.ext.auth.mtls.impl.SpiffeIdentityExtractor}
+ */
+public class SpiffeIdentityExtractorTest
+{
+    SpiffeIdentityExtractor identityExtractor = new SpiffeIdentityExtractor();
+
+    @Test
+    public void testSpiffeIdentity() throws Exception
+    {
+        X509Certificate certificate
+        = CertificateBuilder
+          .builder()
+          .issuerName("CN=Vertx Auth, OU=ssl_test, O=Unknown, L=Unknown, 
ST=Unknown, C=Unknown")
+          .addSanUriName("spiffe://vertx.auth/unitTest/mtls")
+          .buildSelfSigned();
+        assertThat(identityExtractor.validIdentity(new 
CertificateCredentials(certificate)))
+        .isEqualTo("spiffe://vertx.auth/unitTest/mtls");
+    }
+
+    @Test
+    public void testDifferentCertificateType()
+    {
+        Certificate mockCertificate = mock(Certificate.class);
+        assertThatThrownBy(() -> identityExtractor.validIdentity(new 
CertificateCredentials(mockCertificate)))
+        .isInstanceOf(CredentialValidationException.class);
+    }
+
+    @Test
+    public void testNonSpiffeIdentity() throws Exception
+    {
+        X509Certificate certificate
+        = CertificateBuilder
+          .builder()
+          .issuerName("CN=Vertx Auth, OU=ssl_test, O=Unknown, L=Unknown, 
ST=Unknown, C=Unknown")
+          .addSanUriName("randomuri://extracted/from/certificate")
+          .buildSelfSigned();
+        assertThatThrownBy(() -> identityExtractor.validIdentity(new 
CertificateCredentials(certificate)))
+        .isInstanceOf(CredentialValidationException.class)
+        .hasMessage("Unable to extract SPIFFE identity from certificate");
+    }
+
+    @Test
+    public void testInvalidCertificate() throws Exception
+    {
+        X509Certificate certificate
+        = CertificateBuilder
+          .builder()
+          .issuerName("CN=Vertx Auth, OU=ssl_test, O=Unknown, L=Unknown, 
ST=Unknown, C=Unknown")
+          .buildSelfSigned();
+        assertThatThrownBy(() -> identityExtractor.validIdentity(new 
CertificateCredentials(certificate)))
+        .isInstanceOf(CredentialValidationException.class)
+        .hasMessage("Error reading SAN of certificate");
+    }
+
+    @Test
+    public void testNonTrustedDomain() throws Exception
+    {
+        X509Certificate certificate
+        = CertificateBuilder
+          .builder()
+          .issuerName("CN=Vertx Auth, OU=ssl_test, O=Unknown, L=Unknown, 
ST=Unknown, C=Unknown")
+          .addSanUriName("spiffe://nontrusted/unitTest/mtls")
+          .buildSelfSigned();
+        SpiffeIdentityExtractor identityExtractorWithTrust = new 
SpiffeIdentityExtractor("vertx.auth");
+        assertThatThrownBy(() -> identityExtractorWithTrust.validIdentity(new 
CertificateCredentials(certificate)))
+        .isInstanceOf(CredentialValidationException.class)
+        .hasMessage("SPIFFE Identity domain nontrusted is not trusted");
+    }
+
+    @Test
+    public void testNonX509CertificatePeerCertificate()
+    {
+        Certificate certificate = mock(Certificate.class);
+        assertThatThrownBy(() -> identityExtractor.validIdentity(new 
CertificateCredentials(certificate)))
+        .isInstanceOf(CredentialValidationException.class)
+        .hasMessage("No X509Certificate found for validating");
+    }
+}
diff --git 
a/vertx-auth-mtls/src/testFixtures/java/io/vertx/ext/auth/mtls/utils/CertificateBuilder.java
 
b/vertx-auth-mtls/src/testFixtures/java/io/vertx/ext/auth/mtls/utils/CertificateBuilder.java
new file mode 100644
index 00000000..13fe6a42
--- /dev/null
+++ 
b/vertx-auth-mtls/src/testFixtures/java/io/vertx/ext/auth/mtls/utils/CertificateBuilder.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.vertx.ext.auth.mtls.utils;
+
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.security.spec.ECGenParameterSpec;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.cert.CertIOException;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+
+/**
+ * For building certificates for unit testing with specified details such as 
issuer, validity date etc.
+ */
+public class CertificateBuilder
+{
+    private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+    private static final String ALGORITHM = "EC";
+    private static final ECGenParameterSpec ALGORITHM_PARAMETER_SPEC = new 
ECGenParameterSpec("secp256r1");
+    private static final String SIGNATURE_ALGORITHM = "SHA256WITHECDSA";
+    private static final GeneralName[] EMPTY_SAN = {};
+
+    private BigInteger serial = new BigInteger(159, SECURE_RANDOM);
+    private Date notBefore = Date.from(Instant.now().minus(1, 
ChronoUnit.DAYS));
+    private Date notAfter = Date.from(Instant.now().plus(1, ChronoUnit.DAYS));
+    private X500Name issuerName;
+    private final List<GeneralName> subjectAlternativeNames = new 
ArrayList<>();
+
+    public CertificateBuilder serial(BigInteger serial)
+    {
+        this.serial = serial;
+        return this;
+    }
+
+    public CertificateBuilder notBefore(Date notBefore)
+    {
+        this.notBefore = Date.from(notBefore.toInstant());
+        return this;
+    }
+
+    public CertificateBuilder notAfter(Date notAfter)
+    {
+        this.notAfter = Date.from(notBefore.toInstant());
+        return this;
+    }
+
+    public CertificateBuilder issuerName(String issuer)
+    {
+        this.issuerName = new X500Name(issuer);
+        return this;
+    }
+
+    public CertificateBuilder addSanUriName(String uri)
+    {
+        subjectAlternativeNames.add(new 
GeneralName(GeneralName.uniformResourceIdentifier, uri));
+        return this;
+    }
+
+    public static CertificateBuilder builder()
+    {
+        return new CertificateBuilder();
+    }
+
+    public X509Certificate buildSelfSigned() throws GeneralSecurityException, 
CertIOException, OperatorCreationException
+    {
+        KeyPair keyPair = generateKeyPair();
+
+        JcaX509v3CertificateBuilder builder = new 
JcaX509v3CertificateBuilder(issuerName, serial, notBefore, notAfter, 
issuerName, keyPair.getPublic());
+        builder.addExtension(Extension.subjectAlternativeName, false, new 
GeneralNames(subjectAlternativeNames.toArray(EMPTY_SAN)));
+
+        ContentSigner signer = new 
JcaContentSignerBuilder(SIGNATURE_ALGORITHM).build(keyPair.getPrivate());
+        X509CertificateHolder holder = builder.build(signer);
+        return new JcaX509CertificateConverter().getCertificate(holder);
+    }
+
+    private KeyPair generateKeyPair() throws GeneralSecurityException
+    {
+        KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM);
+        keyGen.initialize(ALGORITHM_PARAMETER_SPEC, SECURE_RANDOM);
+        return keyGen.generateKeyPair();
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@cassandra.apache.org
For additional commands, e-mail: commits-h...@cassandra.apache.org

Reply via email to