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