aut
* certificate and matches provided hostname(CN/SAN) with expected broker's host name. It follows RFC 2818, 3.1.
* Server Identity hostname verification.
*
- * @see RFC 818
+ * The CN is only a fallback, and only on the default engines: it is consulted when the client connects by
+ * hostname and the certificate carries no {@code dNSName} SAN, and ignored once any is present. A client that
+ * pins Conscrypt as its JSSE provider verifies against the SAN alone and never falls back to the CN (Pulsar
+ * 5.0, PIP-478). A connection to an IP literal is matched against {@code iPAddress} SANs, never the CN.
+ *
+ * @see RFC 2818
*
* @param enableTlsHostnameVerification whether to enable TLS hostname verification
* @return the client builder instance
diff --git a/pulsar-client-messagecrypto-bc/build.gradle.kts b/pulsar-client-messagecrypto-bc/build.gradle.kts
index 553b714703acc..b0f60b59e94c1 100644
--- a/pulsar-client-messagecrypto-bc/build.gradle.kts
+++ b/pulsar-client-messagecrypto-bc/build.gradle.kts
@@ -27,7 +27,7 @@ dependencies {
api(project(":pulsar-client-api"))
// MessageCryptoBc uses BouncyCastle types directly: bcpkix for PEM parsing (PEMParser,
// JcaPEMKeyConverter) and bcprov for the EC/IES key specs and ASN.1 types used in key handling.
- // The JCA provider itself is resolved at runtime via SecurityUtility (pulsar-common), not here.
+ // The JCA provider itself is resolved at runtime via JcaProviders (pulsar-common), not here.
implementation(libs.bcpkix.jdk18on)
api(libs.bcprov.jdk18on)
implementation(libs.guava)
diff --git a/pulsar-client-messagecrypto-bc/src/main/java/org/apache/pulsar/client/impl/crypto/MessageCryptoBc.java b/pulsar-client-messagecrypto-bc/src/main/java/org/apache/pulsar/client/impl/crypto/MessageCryptoBc.java
index 7b418cd67dcc5..7363cc3ecdb0e 100644
--- a/pulsar-client-messagecrypto-bc/src/main/java/org/apache/pulsar/client/impl/crypto/MessageCryptoBc.java
+++ b/pulsar-client-messagecrypto-bc/src/main/java/org/apache/pulsar/client/impl/crypto/MessageCryptoBc.java
@@ -64,7 +64,7 @@
import org.apache.pulsar.common.api.proto.EncryptionKeys;
import org.apache.pulsar.common.api.proto.KeyValue;
import org.apache.pulsar.common.api.proto.MessageMetadata;
-import org.apache.pulsar.common.util.SecurityUtility;
+import org.apache.pulsar.common.util.tls.JcaProviders;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
@@ -93,15 +93,16 @@ public class MessageCryptoBc implements MessageCrypto
- * An up-to-date list of suffixes can be obtained from publicsuffix.org
- *
- * @since 4.4
- */
-@Data
-public final class PublicSuffixList {
-
- private final DomainType type;
- private final List rules;
- private final List exceptions;
-
- /**
- * @since 4.5
- */
- public PublicSuffixList(final DomainType type, final List rules, final List exceptions) {
- this.type = type;
- this.rules = Collections.unmodifiableList(rules);
- this.exceptions = Collections
- .unmodifiableList(exceptions != null ? exceptions : Collections. emptyList());
- }
-
- public PublicSuffixList(final List rules, final List exceptions) {
- this(DomainType.UNKNOWN, rules, exceptions);
- }
-}
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/tls/PublicSuffixMatcher.java b/pulsar-common/src/main/java/org/apache/pulsar/common/tls/PublicSuffixMatcher.java
deleted file mode 100644
index c4f3e3eb987f7..0000000000000
--- a/pulsar-common/src/main/java/org/apache/pulsar/common/tls/PublicSuffixMatcher.java
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * 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.
- */
-
-/*
- * From Apache HTTP client
- */
-
-package org.apache.pulsar.common.tls;
-
-import java.net.IDN;
-import java.util.Collection;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * Utility class that can test if DNS names match the content of the Public Suffix List.
- *
- * An up-to-date list of suffixes can be obtained from
- * publicsuffix.org
- *
- * @see org.apache.pulsar.common.tls.PublicSuffixList
- *
- * @since 4.4
- */
-public final class PublicSuffixMatcher {
-
- private final Map rules;
- private final Map exceptions;
-
- public PublicSuffixMatcher(final Collection rules, final Collection exceptions) {
- this(DomainType.UNKNOWN, rules, exceptions);
- }
-
- /**
- * @since 4.5
- */
- public PublicSuffixMatcher(
- final DomainType domainType, final Collection rules, final Collection exceptions) {
- this.rules = new ConcurrentHashMap(rules.size());
- for (final String rule: rules) {
- this.rules.put(rule, domainType);
- }
- this.exceptions = new ConcurrentHashMap();
- if (exceptions != null) {
- for (final String exception: exceptions) {
- this.exceptions.put(exception, domainType);
- }
- }
- }
-
- /**
- * @since 4.5
- */
- public PublicSuffixMatcher(final Collection lists) {
- this.rules = new ConcurrentHashMap();
- this.exceptions = new ConcurrentHashMap();
- for (final PublicSuffixList list: lists) {
- final DomainType domainType = list.getType();
- final List rules = list.getRules();
- for (final String rule: rules) {
- this.rules.put(rule, domainType);
- }
- final List exceptions = list.getExceptions();
- if (exceptions != null) {
- for (final String exception: exceptions) {
- this.exceptions.put(exception, domainType);
- }
- }
- }
- }
-
- private static boolean hasEntry(
- final Map map,
- final String rule,
- final DomainType expectedType) {
- if (map == null) {
- return false;
- }
- final DomainType domainType = map.get(rule);
- if (domainType == null) {
- return false;
- } else {
- return expectedType == null || domainType.equals(expectedType);
- }
- }
-
- private boolean hasRule(final String rule, final DomainType expectedType) {
- return hasEntry(this.rules, rule, expectedType);
- }
-
- private boolean hasException(final String exception, final DomainType expectedType) {
- return hasEntry(this.exceptions, exception, expectedType);
- }
-
- /**
- * Returns registrable part of the domain for the given domain name or {@code null}
- * if given domain represents a public suffix.
- *
- * @param domain
- * @return domain root
- */
- public String getDomainRoot(final String domain) {
- return getDomainRoot(domain, null);
- }
-
- /**
- * Returns registrable part of the domain for the given domain name or {@code null}
- * if given domain represents a public suffix.
- *
- * @param domain
- * @param expectedType expected domain type or {@code null} if any.
- * @return domain root
- *
- * @since 4.5
- */
- public String getDomainRoot(final String domain, final DomainType expectedType) {
- if (domain == null) {
- return null;
- }
- if (domain.startsWith(".")) {
- return null;
- }
- String domainName = null;
- String segment = domain.toLowerCase(Locale.ROOT);
- while (segment != null) {
-
- // An exception rule takes priority over any other matching rule.
- if (hasException(IDN.toUnicode(segment), expectedType)) {
- return segment;
- }
-
- if (hasRule(IDN.toUnicode(segment), expectedType)) {
- break;
- }
-
- final int nextdot = segment.indexOf('.');
- final String nextSegment = nextdot != -1 ? segment.substring(nextdot + 1) : null;
-
- if (nextSegment != null) {
- if (hasRule("*." + IDN.toUnicode(nextSegment), expectedType)) {
- break;
- }
- }
- if (nextdot != -1) {
- domainName = segment;
- }
- segment = nextSegment;
- }
- return domainName;
- }
-
- /**
- * Tests whether the given domain matches any of entry from the public suffix list.
- */
- public boolean matches(final String domain) {
- return matches(domain, null);
- }
-
- /**
- * Tests whether the given domain matches any of entry from the public suffix list.
- *
- * @param domain
- * @param expectedType expected domain type or {@code null} if any.
- * @return {@code true} if the given domain matches any of the public suffixes.
- *
- * @since 4.5
- */
- public boolean matches(final String domain, final DomainType expectedType) {
- if (domain == null) {
- return false;
- }
- final String domainRoot = getDomainRoot(
- domain.startsWith(".") ? domain.substring(1) : domain, expectedType);
- return domainRoot == null;
- }
-
-}
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/tls/SubjectName.java b/pulsar-common/src/main/java/org/apache/pulsar/common/tls/SubjectName.java
deleted file mode 100644
index 74542707d9ea8..0000000000000
--- a/pulsar-common/src/main/java/org/apache/pulsar/common/tls/SubjectName.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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.
- */
-
-/*
- * From Apache HTTP client
- */
-
-package org.apache.pulsar.common.tls;
-
-import lombok.Data;
-
-@Data
-final class SubjectName {
-
- static final int DNS = 2;
- static final int IP = 7;
-
- private final String value;
- private final int type;
-
- static SubjectName newIP(final String value) {
- return new SubjectName(value, IP);
- }
-
- static SubjectName newDNS(final String value) {
- return new SubjectName(value, DNS);
- }
-}
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/tls/TlsHostnameVerifier.java b/pulsar-common/src/main/java/org/apache/pulsar/common/tls/TlsHostnameVerifier.java
deleted file mode 100644
index 2bbb9cb34f54c..0000000000000
--- a/pulsar-common/src/main/java/org/apache/pulsar/common/tls/TlsHostnameVerifier.java
+++ /dev/null
@@ -1,313 +0,0 @@
-/*
- * 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.
- */
-
-/*
- * From Apache HTTP client
- */
-
-package org.apache.pulsar.common.tls;
-
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-import java.security.cert.Certificate;
-import java.security.cert.CertificateParsingException;
-import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.NoSuchElementException;
-import javax.naming.InvalidNameException;
-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 javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.SSLException;
-import javax.net.ssl.SSLPeerUnverifiedException;
-import javax.net.ssl.SSLSession;
-import javax.security.auth.x500.X500Principal;
-import lombok.CustomLog;
-
-@CustomLog
-public class TlsHostnameVerifier implements HostnameVerifier {
-
- enum HostNameType {
-
- IPv4(7), IPv6(7), DNS(2);
-
- final int subjectType;
-
- HostNameType(final int subjectType) {
- this.subjectType = subjectType;
- }
- }
-
- private final PublicSuffixMatcher publicSuffixMatcher;
-
- public TlsHostnameVerifier(final PublicSuffixMatcher publicSuffixMatcher) {
- this.publicSuffixMatcher = publicSuffixMatcher;
- }
-
- public TlsHostnameVerifier() {
- this(null);
- }
-
- @Override
- public boolean verify(final String host, final SSLSession session) {
- try {
- final Certificate[] certs = session.getPeerCertificates();
- final X509Certificate x509 = (X509Certificate) certs[0];
- verify(host, x509);
- return true;
- } catch (final SSLException ex) {
- log.debug().exception(ex).log(ex.getMessage());
- return false;
- }
- }
-
- public void verify(
- final String host, final X509Certificate cert) throws SSLException {
- final HostNameType hostType = determineHostFormat(host);
- final List subjectAlts = getSubjectAltNames(cert);
- if (subjectAlts != null && !subjectAlts.isEmpty()) {
- switch (hostType) {
- case IPv4:
- matchIPAddress(host, subjectAlts);
- break;
- case IPv6:
- matchIPv6Address(host, subjectAlts);
- break;
- default:
- matchDNSName(host, subjectAlts, this.publicSuffixMatcher);
- }
- } else {
- // CN matching has been deprecated by rfc2818 and can be used
- // as fallback only when no subjectAlts are available
- final X500Principal subjectPrincipal = cert.getSubjectX500Principal();
- final String cn = extractCN(subjectPrincipal.getName(X500Principal.RFC2253));
- if (cn == null) {
- throw new SSLException("Certificate subject for <" + host + "> doesn't contain "
- + "a common name and does not have alternative names");
- }
- matchCN(host, cn, this.publicSuffixMatcher);
- }
- }
-
- static void matchIPAddress(final String host, final List subjectAlts) throws SSLException {
- for (int i = 0; i < subjectAlts.size(); i++) {
- final SubjectName subjectAlt = subjectAlts.get(i);
- if (subjectAlt.getType() == SubjectName.IP) {
- if (host.equals(subjectAlt.getValue())) {
- return;
- }
- }
- }
- throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any "
- + "of the subject alternative names: " + subjectAlts);
- }
-
- static void matchIPv6Address(final String host, final List subjectAlts) throws SSLException {
- final String normalisedHost = normaliseAddress(host);
- for (int i = 0; i < subjectAlts.size(); i++) {
- final SubjectName subjectAlt = subjectAlts.get(i);
- if (subjectAlt.getType() == SubjectName.IP) {
- final String normalizedSubjectAlt = normaliseAddress(subjectAlt.getValue());
- if (normalisedHost.equals(normalizedSubjectAlt)) {
- return;
- }
- }
- }
- throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any "
- + "of the subject alternative names: " + subjectAlts);
- }
-
- static void matchDNSName(final String host, final List subjectAlts,
- final PublicSuffixMatcher publicSuffixMatcher) throws SSLException {
- final String normalizedHost = host.toLowerCase(Locale.ROOT);
- for (int i = 0; i < subjectAlts.size(); i++) {
- final SubjectName subjectAlt = subjectAlts.get(i);
- if (subjectAlt.getType() == SubjectName.DNS) {
- final String normalizedSubjectAlt = subjectAlt.getValue().toLowerCase(Locale.ROOT);
- if (matchIdentityStrict(normalizedHost, normalizedSubjectAlt, publicSuffixMatcher)) {
- return;
- }
- }
- }
- throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any "
- + "of the subject alternative names: " + subjectAlts);
- }
-
- static void matchCN(final String host, final String cn,
- final PublicSuffixMatcher publicSuffixMatcher) throws SSLException {
- final String normalizedHost = host.toLowerCase(Locale.ROOT);
- final String normalizedCn = cn.toLowerCase(Locale.ROOT);
- if (!matchIdentityStrict(normalizedHost, normalizedCn, publicSuffixMatcher)) {
- throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match "
- + "common name of the certificate subject: " + cn);
- }
- }
-
- static boolean matchDomainRoot(final String host, final String domainRoot) {
- if (domainRoot == null) {
- return false;
- }
- return host.endsWith(domainRoot) && (host.length() == domainRoot.length()
- || host.charAt(host.length() - domainRoot.length() - 1) == '.');
- }
-
- private static boolean matchIdentity(final String host, final String identity,
- final PublicSuffixMatcher publicSuffixMatcher,
- final boolean strict) {
- if (publicSuffixMatcher != null && host.contains(".")) {
- if (!matchDomainRoot(host, publicSuffixMatcher.getDomainRoot(identity, DomainType.ICANN))) {
- return false;
- }
- }
-
- // RFC 2818, 3.1. Server Identity
- // "...Names may contain the wildcard
- // character * which is considered to match any single domain name
- // component or component fragment..."
- // Based on this statement presuming only singular wildcard is legal
- final int asteriskIdx = identity.indexOf('*');
- if (asteriskIdx != -1) {
- final String prefix = identity.substring(0, asteriskIdx);
- final String suffix = identity.substring(asteriskIdx + 1);
- if (!prefix.isEmpty() && !host.startsWith(prefix)) {
- return false;
- }
- if (!suffix.isEmpty() && !host.endsWith(suffix)) {
- return false;
- }
- // Additional sanity checks on content selected by wildcard can be done here
- if (strict) {
- final String remainder = host.substring(
- prefix.length(), host.length() - suffix.length());
- return !remainder.contains(".");
- }
- return true;
- }
- return host.equalsIgnoreCase(identity);
- }
-
- static boolean matchIdentity(final String host, final String identity,
- final PublicSuffixMatcher publicSuffixMatcher) {
- return matchIdentity(host, identity, publicSuffixMatcher, false);
- }
-
- static boolean matchIdentity(final String host, final String identity) {
- return matchIdentity(host, identity, null, false);
- }
-
- static boolean matchIdentityStrict(final String host, final String identity,
- final PublicSuffixMatcher publicSuffixMatcher) {
- return matchIdentity(host, identity, publicSuffixMatcher, true);
- }
-
- static boolean matchIdentityStrict(final String host, final String identity) {
- return matchIdentity(host, identity, null, true);
- }
-
- static String extractCN(final String subjectPrincipal) throws SSLException {
- if (subjectPrincipal == null) {
- return null;
- }
- try {
- final LdapName subjectDN = new LdapName(subjectPrincipal);
- final List rdns = subjectDN.getRdns();
- for (int i = rdns.size() - 1; i >= 0; i--) {
- final Rdn rds = rdns.get(i);
- final Attributes attributes = rds.toAttributes();
- final Attribute cn = attributes.get("cn");
- if (cn != null) {
- try {
- final Object value = cn.get();
- if (value != null) {
- return value.toString();
- }
- } catch (final NoSuchElementException ignore) {
- // ignore exception
- } catch (final NamingException ignore) {
- // ignore exception
- }
- }
- }
- return null;
- } catch (final InvalidNameException e) {
- throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name");
- }
- }
-
- static HostNameType determineHostFormat(final String host) {
- if (InetAddressUtils.isIPv4Address(host)) {
- return HostNameType.IPv4;
- }
- String s = host;
- if (s.startsWith("[") && s.endsWith("]")) {
- s = host.substring(1, host.length() - 1);
- }
- if (InetAddressUtils.isIPv6Address(s)) {
- return HostNameType.IPv6;
- }
- return HostNameType.DNS;
- }
-
- static List getSubjectAltNames(final X509Certificate cert) {
- try {
- final Collection> entries = cert.getSubjectAlternativeNames();
- if (entries == null) {
- return Collections.emptyList();
- }
- final List result = new ArrayList();
- for (final List> entry : entries) {
- final Integer type = entry.size() >= 2 ? (Integer) entry.get(0) : null;
- if (type != null) {
- final Object o = entry.get(1);
- if (o instanceof String) {
- result.add(new SubjectName((String) o, type));
- } else if (o instanceof byte[]) {
- // TODO ASN.1 DER encoded form
- }
- }
- }
- return result;
- } catch (final CertificateParsingException ignore) {
- return Collections.emptyList();
- }
- }
-
- /*
- * Normalize IPv6 or DNS name.
- */
- static String normaliseAddress(final String hostname) {
- if (hostname == null) {
- return hostname;
- }
- try {
- final InetAddress inetAddress = InetAddress.getByName(hostname);
- return inetAddress.getHostAddress();
- } catch (final UnknownHostException unexpected) { // Should not happen, because we check for IPv6 address above
- return hostname;
- }
- }
-
-}
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/tls/impl/TlsContexts.java b/pulsar-common/src/main/java/org/apache/pulsar/common/tls/impl/TlsContexts.java
index e520cdfa344c6..984f36cb6fc1b 100644
--- a/pulsar-common/src/main/java/org/apache/pulsar/common/tls/impl/TlsContexts.java
+++ b/pulsar-common/src/main/java/org/apache/pulsar/common/tls/impl/TlsContexts.java
@@ -73,7 +73,7 @@ public final class TlsContexts {
/**
* The default enabled TLS protocol set, applied whenever the effective protocol list is empty — a policy
* with no {@code protocols()}, or a synthesized context whose factory companion set none. This preserves
- * the {@code {TLSv1.3, TLSv1.2}} floor {@code DefaultPulsarSslFactory} forces at engine build:
+ * the {@code {TLSv1.3, TLSv1.2}} floor v4's {@code DefaultPulsarSslFactory} forced at engine build:
* without it the PIP-478 path would silently defer to the JVM/provider default protocol set, a
* security-relevant drift on upgrade.
*/
@@ -491,7 +491,7 @@ private static void applyCiphersAndProtocols(SslContextBuilder builder, TlsPolic
builder.ciphers(ciphers);
}
// Pin the enabled protocols even when the policy configured none, preserving the {TLSv1.3, TLSv1.2}
- // floor DefaultPulsarSslFactory forces rather than deferring to the provider default.
+ // floor v4's DefaultPulsarSslFactory forced rather than deferring to the provider default.
String[] enabledProtocols = protocols != null
? protocols.toArray(new String[0])
: DEFAULT_ENABLED_PROTOCOLS.toArray(new String[0]);
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/tls/package-info.java b/pulsar-common/src/main/java/org/apache/pulsar/common/tls/package-info.java
index 204d21194ef24..8d1120c1a2d7d 100644
--- a/pulsar-common/src/main/java/org/apache/pulsar/common/tls/package-info.java
+++ b/pulsar-common/src/main/java/org/apache/pulsar/common/tls/package-info.java
@@ -25,9 +25,14 @@
* stacks. They carry no dependency beyond the JDK and slog.
*
* Hostname verification is delegated to the JDK/provider standard endpoint identification
- * ({@code endpointIdentificationAlgorithm = "HTTPS"}), i.e. SAN-based (RFC 2818) matching. The deprecated
- * custom CN-based hostname verifier was removed in Pulsar 5.0 (PIP-478); certificates must carry the hostname
- * in the SubjectAltName (SAN) extension.
+ * ({@code endpointIdentificationAlgorithm = "HTTPS"}). Pulsar's own CN-based hostname verifier was removed in
+ * 5.0 (PIP-478), so verification is whatever the configured provider implements. Note that the standard
+ * algorithm is not SAN-only: per RFC 2818 §3.1, the JDK and OpenSSL engines consult the CN when the client
+ * connects by hostname and the certificate carries no {@code dNSName} SAN, and ignore it once any is
+ * present — behaviour RFC 9525 has since retired, which is why Conscrypt does not implement it. The
+ * fallback is for hostnames only — a client connecting to an IP literal is matched against {@code iPAddress}
+ * SANs and never against the CN. Conscrypt does not fall back to the CN at all; pinning it is what makes
+ * verification strictly SAN-based.
*
*
The purpose-driven TLS SPI ({@code PulsarTlsFactory} and companions) lives in its own focused module
* under {@link org.apache.pulsar.tls}; the default {@code FileBasedTlsFactory} implementation lives in the
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/util/DefaultPulsarSslFactory.java b/pulsar-common/src/main/java/org/apache/pulsar/common/util/DefaultPulsarSslFactory.java
deleted file mode 100644
index b7902b4543fc5..0000000000000
--- a/pulsar-common/src/main/java/org/apache/pulsar/common/util/DefaultPulsarSslFactory.java
+++ /dev/null
@@ -1,373 +0,0 @@
-/*
- * 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 org.apache.pulsar.common.util;
-
-import io.netty.buffer.ByteBufAllocator;
-import io.netty.handler.ssl.SslContext;
-import io.netty.handler.ssl.SslProvider;
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.util.concurrent.atomic.AtomicReference;
-import javax.annotation.concurrent.NotThreadSafe;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLEngine;
-import javax.net.ssl.SSLParameters;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.pulsar.client.api.AuthenticationDataProvider;
-import org.apache.pulsar.client.api.KeyStoreParams;
-import org.apache.pulsar.common.util.keystoretls.KeyStoreSSLContext;
-
-/**
- * Default Implementation of {@link PulsarSslFactory}. This factory loads file based certificates to create SSLContext
- * and SSL Engines. This class is not thread safe. It has been integrated into the pulsar code base as a single writer,
- * multiple readers pattern.
- */
-@NotThreadSafe
-public class DefaultPulsarSslFactory implements PulsarSslFactory {
-
- private PulsarSslConfiguration config;
- private final AtomicReference internalSslContext = new AtomicReference<>();
- private final AtomicReference internalNettySslContext = new AtomicReference<>();
-
- protected FileModifiedTimeUpdater tlsKeyStore;
- protected FileModifiedTimeUpdater tlsTrustStore;
- protected FileModifiedTimeUpdater tlsTrustCertsFilePath;
- protected FileModifiedTimeUpdater tlsCertificateFilePath;
- protected FileModifiedTimeUpdater tlsKeyFilePath;
- protected AuthenticationDataProvider authData;
- protected boolean isTlsTrustStoreStreamProvided;
- protected final String[] defaultSslEnabledProtocols = {"TLSv1.3", "TLSv1.2"};
- protected String tlsKeystoreType;
- protected String tlsKeystorePath;
- protected String tlsKeystorePassword;
-
- /**
- * Initializes the DefaultPulsarSslFactory.
- *
- * @param config {@link PulsarSslConfiguration} object required for initialization.
- *
- */
- @Override
- public void initialize(PulsarSslConfiguration config) {
- this.config = config;
- AuthenticationDataProvider authData = this.config.getAuthData();
- if (this.config.isTlsEnabledWithKeystore()) {
- if (authData != null && authData.hasDataForTls()) {
- KeyStoreParams authParams = authData.getTlsKeyStoreParams();
- if (authParams != null) {
- this.tlsKeystoreType = authParams.getKeyStoreType();
- this.tlsKeystorePath = authParams.getKeyStorePath();
- this.tlsKeystorePassword = authParams.getKeyStorePassword();
- }
- }
- if (this.tlsKeystoreType == null) {
- this.tlsKeystoreType = this.config.getTlsKeyStoreType();
- }
- if (this.tlsKeystorePath == null) {
- this.tlsKeystorePath = this.config.getTlsKeyStorePath();
- }
- if (this.tlsKeystorePassword == null) {
- this.tlsKeystorePassword = this.config.getTlsKeyStorePassword();
- }
- this.tlsKeyStore = new FileModifiedTimeUpdater(this.tlsKeystorePath);
- this.tlsTrustStore = new FileModifiedTimeUpdater(this.config.getTlsTrustStorePath());
- } else {
- if (authData != null && authData.hasDataForTls()) {
- if (authData.getTlsTrustStoreStream() != null) {
- this.isTlsTrustStoreStreamProvided = true;
- } else {
- this.tlsTrustCertsFilePath = new FileModifiedTimeUpdater(this.config.getTlsTrustCertsFilePath());
- }
- this.authData = authData;
- } else {
- this.tlsCertificateFilePath = new FileModifiedTimeUpdater(this.config.getTlsCertificateFilePath());
- this.tlsTrustCertsFilePath = new FileModifiedTimeUpdater(this.config.getTlsTrustCertsFilePath());
- this.tlsKeyFilePath = new FileModifiedTimeUpdater(this.config.getTlsKeyFilePath());
- }
- }
- }
-
- /**
- * Creates a Client {@link SSLEngine} utilizing the peer hostname, peer port and {@link PulsarSslConfiguration}
- * object provided during initialization.
- *
- * @param peerHost the name of the peer host
- * @param peerPort the port number of the peer
- * @return {@link SSLEngine}
- */
- @Override
- public SSLEngine createClientSslEngine(ByteBufAllocator buf, String peerHost, int peerPort) {
- return createSSLEngine(buf, peerHost, peerPort, NetworkMode.CLIENT);
- }
-
- /**
- * Creates a Server {@link SSLEngine} utilizing the {@link PulsarSslConfiguration} object provided during
- * initialization.
- *
- * @return {@link SSLEngine}
- */
- @Override
- public SSLEngine createServerSslEngine(ByteBufAllocator buf) {
- return createSSLEngine(buf, "", 0, NetworkMode.SERVER);
- }
-
- /**
- * Returns a boolean value based on if the underlying certificate files have been modified since it was last read.
- *
- * @return {@code true} if the underlying certificates have been modified indicating that
- * the SSL Context should be refreshed.
- */
- @Override
- public boolean needsUpdate() {
- if (this.config.isTlsEnabledWithKeystore()) {
- return (this.tlsKeyStore != null && this.tlsKeyStore.checkAndRefresh())
- || (this.tlsTrustStore != null && this.tlsTrustStore.checkAndRefresh());
- } else {
- if (this.authData != null && this.authData.hasDataForTls()) {
- return true;
- } else {
- return this.tlsTrustCertsFilePath.checkAndRefresh() || this.tlsCertificateFilePath.checkAndRefresh()
- || this.tlsKeyFilePath.checkAndRefresh();
- }
- }
- }
-
- /**
- * Creates a {@link SSLContext} object and saves it internally.
- *
- * @throws Exception If there were any issues generating the {@link SSLContext}
- */
- @Override
- public void createInternalSslContext() throws Exception {
- if (this.config.isTlsEnabledWithKeystore()) {
- this.internalSslContext.set(buildKeystoreSslContext(this.config.isServerMode()));
- } else {
- if (this.config.isHttps()) {
- this.internalSslContext.set(buildSslContext());
- } else {
- this.internalNettySslContext.set(buildNettySslContext());
- }
- }
- }
-
-
- /**
- * Get the internally stored {@link SSLContext}.
- *
- * @return {@link SSLContext}
- * @throws RuntimeException if the {@link SSLContext} object has not yet been initialized.
- */
- @Override
- public SSLContext getInternalSslContext() {
- if (this.internalSslContext.get() == null) {
- throw new RuntimeException("Internal SSL context is not initialized. "
- + "Please call createInternalSslContext() first.");
- }
- return this.internalSslContext.get();
- }
-
- /**
- * Get the internally stored {@link SslContext}.
- *
- * @return {@link SslContext}
- * @throws RuntimeException if the {@link SslContext} object has not yet been initialized.
- */
- public SslContext getInternalNettySslContext() {
- if (this.internalNettySslContext.get() == null) {
- throw new RuntimeException("Internal SSL context is not initialized. "
- + "Please call createInternalSslContext() first.");
- }
- return this.internalNettySslContext.get();
- }
-
- private SSLContext buildKeystoreSslContext(boolean isServerMode) throws GeneralSecurityException, IOException {
- KeyStoreSSLContext keyStoreSSLContext;
- if (isServerMode) {
- keyStoreSSLContext = KeyStoreSSLContext.createServerKeyStoreSslContext(this.config.getTlsProvider(),
- this.tlsKeystoreType, this.tlsKeyStore.getFileName(),
- this.tlsKeystorePassword, this.config.isAllowInsecureConnection(),
- this.config.getTlsTrustStoreType(), this.tlsTrustStore.getFileName(),
- this.config.getTlsTrustStorePassword(), this.config.isRequireTrustedClientCertOnConnect(),
- this.config.getTlsCiphers(), this.config.getTlsProtocols());
- } else {
- keyStoreSSLContext = KeyStoreSSLContext.createClientKeyStoreSslContext(this.config.getTlsProvider(),
- this.tlsKeystoreType, this.tlsKeyStore.getFileName(),
- this.tlsKeystorePassword, this.config.isAllowInsecureConnection(),
- this.config.getTlsTrustStoreType(), this.tlsTrustStore.getFileName(),
- this.config.getTlsTrustStorePassword(), this.config.getTlsCiphers(),
- this.config.getTlsProtocols());
- }
- return keyStoreSSLContext.createSSLContext();
- }
-
- private SSLContext buildSslContext() throws GeneralSecurityException {
- if (this.authData != null && this.authData.hasDataForTls()) {
- if (this.isTlsTrustStoreStreamProvided) {
- return SecurityUtility.createSslContext(this.config.isAllowInsecureConnection(),
- SecurityUtility.loadCertificatesFromPemStream(this.authData.getTlsTrustStoreStream()),
- this.authData.getTlsCertificates(),
- this.authData.getTlsPrivateKey(),
- this.config.getTlsProvider());
- } else {
- if (this.authData.getTlsCertificates() != null) {
- return SecurityUtility.createSslContext(this.config.isAllowInsecureConnection(),
- SecurityUtility.loadCertificatesFromPemFile(this.tlsTrustCertsFilePath.getFileName()),
- this.authData.getTlsCertificates(),
- this.authData.getTlsPrivateKey(),
- this.config.getTlsProvider());
- } else {
- return SecurityUtility.createSslContext(this.config.isAllowInsecureConnection(),
- this.tlsTrustCertsFilePath.getFileName(),
- this.authData.getTlsCertificateFilePath(),
- this.authData.getTlsPrivateKeyFilePath(),
- this.config.getTlsProvider()
- );
- }
- }
- } else {
- return SecurityUtility.createSslContext(this.config.isAllowInsecureConnection(),
- this.tlsTrustCertsFilePath.getFileName(),
- this.tlsCertificateFilePath.getFileName(),
- this.tlsKeyFilePath.getFileName(),
- this.config.getTlsProvider());
- }
- }
-
- private SslContext buildNettySslContext() throws GeneralSecurityException, IOException {
- SslProvider sslProvider = null;
- if (StringUtils.isNotBlank(this.config.getTlsProvider())) {
- sslProvider = SslProvider.valueOf(this.config.getTlsProvider());
- }
- if (this.authData != null && this.authData.hasDataForTls()) {
- if (this.isTlsTrustStoreStreamProvided) {
- return SecurityUtility.createNettySslContextForClient(sslProvider,
- this.config.isAllowInsecureConnection(),
- this.authData.getTlsTrustStoreStream(),
- this.authData.getTlsCertificates(),
- this.authData.getTlsPrivateKey(),
- this.config.getTlsCiphers(),
- this.config.getTlsProtocols());
- } else {
- if (this.authData.getTlsCertificates() != null) {
- return SecurityUtility.createNettySslContextForClient(sslProvider,
- this.config.isAllowInsecureConnection(),
- this.tlsTrustCertsFilePath.getFileName(),
- this.authData.getTlsCertificates(),
- this.authData.getTlsPrivateKey(),
- this.config.getTlsCiphers(),
- this.config.getTlsProtocols());
- } else {
- return SecurityUtility.createNettySslContextForClient(sslProvider,
- this.config.isAllowInsecureConnection(),
- this.tlsTrustCertsFilePath.getFileName(),
- this.authData.getTlsCertificateFilePath(),
- this.authData.getTlsPrivateKeyFilePath(),
- this.config.getTlsCiphers(),
- this.config.getTlsProtocols());
- }
- }
- } else {
- if (this.config.isServerMode()) {
- return SecurityUtility.createNettySslContextForServer(sslProvider,
- this.config.isAllowInsecureConnection(),
- this.tlsTrustCertsFilePath.getFileName(),
- this.tlsCertificateFilePath.getFileName(),
- this.tlsKeyFilePath.getFileName(),
- this.config.getTlsCiphers(),
- this.config.getTlsProtocols(),
- this.config.isRequireTrustedClientCertOnConnect());
- } else {
- return SecurityUtility.createNettySslContextForClient(sslProvider,
- this.config.isAllowInsecureConnection(),
- this.tlsTrustCertsFilePath.getFileName(),
- this.tlsCertificateFilePath.getFileName(),
- this.tlsKeyFilePath.getFileName(),
- this.config.getTlsCiphers(),
- this.config.getTlsProtocols());
- }
- }
- }
-
- private SSLEngine createSSLEngine(ByteBufAllocator buf, String peerHost, int peerPort, NetworkMode mode) {
- SSLEngine sslEngine;
- SSLParameters sslParams;
- SSLContext sslContext = this.internalSslContext.get();
- SslContext nettySslContext = this.internalNettySslContext.get();
- validateSslContext(sslContext, nettySslContext);
- if (mode == NetworkMode.CLIENT) {
- if (sslContext != null) {
- sslEngine = sslContext.createSSLEngine(peerHost, peerPort);
- } else {
- sslEngine = nettySslContext.newEngine(buf, peerHost, peerPort);
- }
- sslEngine.setUseClientMode(true);
- sslParams = sslEngine.getSSLParameters();
- } else {
- if (sslContext != null) {
- sslEngine = sslContext.createSSLEngine();
- } else {
- sslEngine = nettySslContext.newEngine(buf);
- }
- sslEngine.setUseClientMode(false);
- sslParams = sslEngine.getSSLParameters();
- if (this.config.isRequireTrustedClientCertOnConnect()) {
- sslParams.setNeedClientAuth(true);
- } else {
- sslParams.setWantClientAuth(true);
- }
- }
- // Netty 4.2 changed SslContext.newEngine(alloc, peerHost, peerPort) to default the endpoint
- // identification algorithm to "HTTPS" (it was unset in 4.1). Pulsar applies TLS hostname
- // verification separately via SecurityUtility.configureSSLHandler(), and only when
- // tlsHostnameVerificationEnable is set. Clear it here so the engine does not perform unintended
- // hostname verification (which would otherwise fail whenever the peer host is absent from the
- // certificate SANs, e.g. internal "localhost" broker connections).
- sslParams.setEndpointIdentificationAlgorithm(null);
- if (this.config.getTlsProtocols() != null && !this.config.getTlsProtocols().isEmpty()) {
- sslParams.setProtocols(this.config.getTlsProtocols().toArray(new String[0]));
- } else {
- sslParams.setProtocols(defaultSslEnabledProtocols);
- }
- if (this.config.getTlsCiphers() != null && !this.config.getTlsCiphers().isEmpty()) {
- sslParams.setCipherSuites(this.config.getTlsCiphers().toArray(new String[0]));
- }
- sslEngine.setSSLParameters(sslParams);
- return sslEngine;
- }
-
- private void validateSslContext(SSLContext sslContext, SslContext nettySslContext) {
- if (sslContext == null && nettySslContext == null) {
- throw new RuntimeException("Internal SSL context is not initialized. "
- + "Please call createInternalSslContext() first.");
- }
- }
-
- /**
- * Clean any resources that may have been created.
- * @throws Exception if any resources failed to be cleaned.
- */
- @Override
- public void close() throws Exception {
- // noop
- }
-
- private enum NetworkMode {
- CLIENT, SERVER
- }
-}
\ No newline at end of file
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/util/KeyManagerProxy.java b/pulsar-common/src/main/java/org/apache/pulsar/common/util/KeyManagerProxy.java
deleted file mode 100644
index 4e2ac76b7cb1a..0000000000000
--- a/pulsar-common/src/main/java/org/apache/pulsar/common/util/KeyManagerProxy.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * 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 org.apache.pulsar.common.util;
-
-import io.netty.handler.ssl.SslContext;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.Socket;
-import java.security.KeyManagementException;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.Principal;
-import java.security.PrivateKey;
-import java.security.UnrecoverableKeyException;
-import java.security.cert.Certificate;
-import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
-import java.util.List;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
-import javax.net.ssl.KeyManagerFactory;
-import javax.net.ssl.SSLEngine;
-import javax.net.ssl.X509ExtendedKeyManager;
-import lombok.CustomLog;
-
-/**
- * This class wraps {@link X509ExtendedKeyManager} and gives opportunity to refresh key-manager with refreshed certs
- * without changing {@link SslContext}.
- */
-@CustomLog
-public class KeyManagerProxy extends X509ExtendedKeyManager {
-
- private static final char[] KEYSTORE_PASSWORD = "secret".toCharArray();
- private volatile X509ExtendedKeyManager keyManager;
- private FileModifiedTimeUpdater certFile, keyFile;
-
- public KeyManagerProxy(String certFilePath, String keyFilePath, int refreshDurationSec,
- ScheduledExecutorService executor) {
- this.certFile = new FileModifiedTimeUpdater(certFilePath);
- this.keyFile = new FileModifiedTimeUpdater(keyFilePath);
- try {
- updateKeyManager();
- } catch (CertificateException e) {
- log.warn().attr("certFile", certFile).exception(e).log("Failed to load cert");
- throw new IllegalArgumentException(e);
- } catch (KeyStoreException e) {
- log.warn().attr("keyFile", keyFile).exception(e).log("Failed to load key");
- throw new IllegalArgumentException(e);
- } catch (NoSuchAlgorithmException | UnrecoverableKeyException e) {
- log.warn().exception(e).log("Failed to update key Manager");
- throw new IllegalArgumentException(e);
- }
- executor.scheduleWithFixedDelay(() -> updateKeyManagerSafely(), refreshDurationSec, refreshDurationSec,
- TimeUnit.SECONDS);
- }
-
- private void updateKeyManagerSafely() {
- try {
- log.debug().attr("certFile", certFile.getFileName()).attr("keyFile", keyFile.getFileName())
- .log("refreshing key manager");
- updateKeyManager();
- } catch (Exception e) {
- log.warn().attr("certFile", certFile.getFileName()).attr("keyFile", keyFile.getFileName())
- .exception(e).log("Failed to update key Manager");
- }
- }
-
- private void updateKeyManager()
- throws CertificateException, KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
- if (keyManager != null && !certFile.checkAndRefresh() && !keyFile.checkAndRefresh()) {
- return;
- }
-
- final KeyStore keyStore;
- try (InputStream publicCertStream = new FileInputStream(certFile.getFileName())) {
- final CertificateFactory cf = CertificateFactory.getInstance("X.509");
- final List certificateList = cf.generateCertificates(publicCertStream)
- .stream().map(o -> (X509Certificate) o).collect(Collectors.toList());
- keyStore = KeyStore.getInstance("JKS");
- final String alias = certificateList.get(0).getSubjectX500Principal().getName();
- final PrivateKey privateKey = SecurityUtility.loadPrivateKeyFromPemFile(keyFile.getFileName());
- keyStore.load(null);
- keyStore.setKeyEntry(alias, privateKey, KEYSTORE_PASSWORD, certificateList.toArray(new Certificate[0]));
- } catch (IOException | KeyManagementException e) {
- throw new IllegalArgumentException(e);
- }
-
- final KeyManagerFactory keyManagerFactory = KeyManagerFactory
- .getInstance(KeyManagerFactory.getDefaultAlgorithm());
- keyManagerFactory.init(keyStore, KEYSTORE_PASSWORD);
- this.keyManager = (X509ExtendedKeyManager) keyManagerFactory.getKeyManagers()[0];
- }
-
- @Override
- public String[] getClientAliases(String s, Principal[] principals) {
- return keyManager.getClientAliases(s, principals);
- }
-
- @Override
- public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
- return keyManager.chooseClientAlias(strings, principals, socket);
- }
-
- @Override
- public String[] getServerAliases(String s, Principal[] principals) {
- return keyManager.getServerAliases(s, principals);
- }
-
- @Override
- public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
- return keyManager.chooseServerAlias(s, principals, socket);
- }
-
- @Override
- public X509Certificate[] getCertificateChain(String s) {
- return keyManager.getCertificateChain(s);
- }
-
- @Override
- public PrivateKey getPrivateKey(String s) {
- return keyManager.getPrivateKey(s);
- }
-
- @Override
- public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
- return keyManager.chooseEngineClientAlias(keyType, issuers, engine);
- }
-
- @Override
- public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
- return keyManager.chooseEngineServerAlias(keyType, issuers, engine);
- }
-
-}
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/util/PulsarSslConfiguration.java b/pulsar-common/src/main/java/org/apache/pulsar/common/util/PulsarSslConfiguration.java
deleted file mode 100644
index 86e566c12f049..0000000000000
--- a/pulsar-common/src/main/java/org/apache/pulsar/common/util/PulsarSslConfiguration.java
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * 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 org.apache.pulsar.common.util;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import java.io.Serializable;
-import java.util.Set;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.ToString;
-import org.apache.pulsar.client.api.AuthenticationDataProvider;
-
-/**
- * Pulsar SSL Configuration Object to be used by all Pulsar Server and Client Components.
- */
-@Builder
-@Getter
-@ToString
-public class PulsarSslConfiguration implements Serializable, Cloneable {
-
- private static final long serialVersionUID = 1L;
-
- @Schema(
- name = "tlsCiphers",
- description = "TLS ciphers to be used",
- requiredMode = Schema.RequiredMode.REQUIRED
- )
- private Set tlsCiphers;
-
- @Schema(
- name = "tlsProtocols",
- description = "TLS protocols to be used",
- requiredMode = Schema.RequiredMode.REQUIRED
- )
- private Set tlsProtocols;
-
- @Schema(
- name = "allowInsecureConnection",
- description = "Insecure Connections are allowed",
- requiredMode = Schema.RequiredMode.REQUIRED
- )
- private boolean allowInsecureConnection;
-
- @Schema(
- name = "requireTrustedClientCertOnConnect",
- description = "Require trusted client certificate on connect",
- requiredMode = Schema.RequiredMode.REQUIRED
- )
- private boolean requireTrustedClientCertOnConnect;
-
- @Schema(
- name = "authData",
- description = "Authentication Data Provider utilized by the Client for identification"
- )
- private AuthenticationDataProvider authData;
-
- @Schema(
- name = "tlsCustomParams",
- description = "Custom Parameters required by Pulsar SSL factory plugins"
- )
- private String tlsCustomParams;
-
- @Schema(
- name = "tlsProvider",
- description = "TLS Provider to be used"
- )
- private String tlsProvider;
-
- @Schema(
- name = "tlsTrustStoreType",
- description = "TLS Trust Store Type to be used"
- )
- private String tlsTrustStoreType;
-
- @Schema(
- name = "tlsTrustStorePath",
- description = "TLS Trust Store Path"
- )
- private String tlsTrustStorePath;
-
- @Schema(
- name = "tlsTrustStorePassword",
- description = "TLS Trust Store Password"
- )
- private String tlsTrustStorePassword;
-
- @Schema(
- name = "tlsTrustCertsFilePath",
- description = "TLS Trust certificates file path"
- )
- private String tlsTrustCertsFilePath;
-
- @Schema(
- name = "tlsCertificateFilePath",
- description = "Path for the TLS Certificate file"
- )
- private String tlsCertificateFilePath;
-
- @Schema(
- name = "tlsKeyFilePath",
- description = "Path for TLS Private key file"
- )
- private String tlsKeyFilePath;
-
- @Schema(
- name = "tlsKeyStoreType",
- description = "TLS Key Store Type to be used"
- )
- private String tlsKeyStoreType;
-
- @Schema(
- name = "tlsKeyStorePath",
- description = "TLS Key Store Path"
- )
- private String tlsKeyStorePath;
-
- @Schema(
- name = "tlsKeyStorePassword",
- description = "TLS Key Store Password"
- )
- private String tlsKeyStorePassword;
-
- @Schema(
- name = "isTlsEnabledWithKeystore",
- description = "TLS configuration enabled with key store configs"
- )
- private boolean tlsEnabledWithKeystore;
-
- @Schema(
- name = "isServerMode",
- description = "Is the SSL Configuration for a Server or Client",
- requiredMode = Schema.RequiredMode.REQUIRED
- )
- private boolean serverMode;
-
- @Schema(
- name = "isHttps",
- description = "Is the SSL Configuration for a Http client or Server"
- )
- private boolean isHttps;
-
- @Override
- public PulsarSslConfiguration clone() {
- try {
- return (PulsarSslConfiguration) super.clone();
- } catch (CloneNotSupportedException e) {
- throw new RuntimeException("Failed to clone PulsarSslConfiguration", e);
- }
- }
-
-}
\ No newline at end of file
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/util/PulsarSslFactory.java b/pulsar-common/src/main/java/org/apache/pulsar/common/util/PulsarSslFactory.java
deleted file mode 100644
index bccbbbe5b2516..0000000000000
--- a/pulsar-common/src/main/java/org/apache/pulsar/common/util/PulsarSslFactory.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * 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 org.apache.pulsar.common.util;
-
-import io.netty.buffer.ByteBufAllocator;
-import io.netty.handler.ssl.SslContext;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLEngine;
-
-/**
- * Factory for generating SSL Context and SSL Engine using {@link PulsarSslConfiguration}.
- */
-public interface PulsarSslFactory extends AutoCloseable {
-
- /**
- * Initializes the PulsarSslFactory.
- * @param config {@link PulsarSslConfiguration} object required for initialization
- */
- void initialize(PulsarSslConfiguration config);
-
- /**
- * Creates a Client {@link SSLEngine} utilizing {@link ByteBufAllocator} object, the peer hostname, peer port and
- * {@link PulsarSslConfiguration} object provided during initialization.
- *
- * @param buf The ByteBufAllocator required for netty connections. This can be passed as {@code null} if utilized
- * for web connections.
- * @param peerHost the name of the peer host
- * @param peerPort the port number of the peer
- * @return {@link SSLEngine}
- */
- SSLEngine createClientSslEngine(ByteBufAllocator buf, String peerHost, int peerPort);
-
- /**
- * Creates a Server {@link SSLEngine} utilizing the {@link ByteBufAllocator} object and
- * {@link PulsarSslConfiguration} object provided during initialization.
- *
- * @param buf The ByteBufAllocator required for netty connections. This can be passed as {@code null} if utilized
- * for web connections.
- * @return {@link SSLEngine}
- */
- SSLEngine createServerSslEngine(ByteBufAllocator buf);
-
- /**
- * Returns a boolean value indicating {@link SSLContext} or {@link SslContext} should be refreshed.
- *
- * @return {@code true} if {@link SSLContext} or {@link SslContext} should be refreshed.
- */
- boolean needsUpdate();
-
- /**
- * Update the internal {@link SSLContext} or {@link SslContext}.
- * @throws Exception if there are any issues generating the new {@link SSLContext} or {@link SslContext}
- */
- default void update() throws Exception {
- if (this.needsUpdate()) {
- this.createInternalSslContext();
- }
- }
-
- /**
- * Creates the following:
- * 1. {@link SslContext} if netty connections are being created for Non-Keystore based TLS configurations.
- * 2. {@link SSLContext} if netty connections are being created for Keystore based TLS configurations. It will
- * also create it for all web connections irrespective of it being Keystore or Non-Keystore based TLS
- * configurations.
- *
- * @throws Exception if there are any issues creating the new {@link SSLContext} or {@link SslContext}
- */
- void createInternalSslContext() throws Exception;
-
- /**
- * Get the internally stored {@link SSLContext}. It will be used in the following scenarios:
- * 1. Netty connection creations for keystore based TLS configurations
- * 2. All Web connections
- *
- * @return {@link SSLContext}
- * @throws RuntimeException if the {@link SSLContext} object has not yet been initialized.
- */
- SSLContext getInternalSslContext();
-
- /**
- * Get the internally stored {@link SslContext}. It will be used to create Netty Connections for non-keystore based
- * tls configurations.
- *
- * @return {@link SslContext}
- * @throws RuntimeException if the {@link SslContext} object has not yet been initialized.
- */
- SslContext getInternalNettySslContext();
-
-}
\ No newline at end of file
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/util/SecurityUtility.java b/pulsar-common/src/main/java/org/apache/pulsar/common/util/SecurityUtility.java
deleted file mode 100644
index c8746adba7993..0000000000000
--- a/pulsar-common/src/main/java/org/apache/pulsar/common/util/SecurityUtility.java
+++ /dev/null
@@ -1,550 +0,0 @@
-/*
- * 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 org.apache.pulsar.common.util;
-
-import io.netty.handler.ssl.ClientAuth;
-import io.netty.handler.ssl.SslContext;
-import io.netty.handler.ssl.SslContextBuilder;
-import io.netty.handler.ssl.SslHandler;
-import io.netty.handler.ssl.SslProvider;
-import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.lang.reflect.Method;
-import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
-import java.security.KeyFactory;
-import java.security.KeyManagementException;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.security.Provider;
-import java.security.SecureRandom;
-import java.security.Security;
-import java.security.UnrecoverableKeyException;
-import java.security.cert.Certificate;
-import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.KeySpec;
-import java.security.spec.PKCS8EncodedKeySpec;
-import java.util.ArrayList;
-import java.util.Base64;
-import java.util.Collection;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.ScheduledExecutorService;
-import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.KeyManager;
-import javax.net.ssl.KeyManagerFactory;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLEngine;
-import javax.net.ssl.SSLException;
-import javax.net.ssl.SSLParameters;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.TrustManagerFactory;
-import lombok.CustomLog;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.pulsar.common.tls.TlsHostnameVerifier;
-
-/**
- * Helper class for the security domain.
- */
-@CustomLog
-public class SecurityUtility {
-
- public static final Provider BC_PROVIDER = getProvider();
- public static final String BC_FIPS_PROVIDER_CLASS = "org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider";
- public static final String BC_NON_FIPS_PROVIDER_CLASS = "org.bouncycastle.jce.provider.BouncyCastleProvider";
- public static final String CONSCRYPT_PROVIDER_CLASS = "org.conscrypt.OpenSSLProvider";
- public static final Provider CONSCRYPT_PROVIDER = loadConscryptProvider();
- private static final List KEY_FACTORY_ALGORITHMS = List.of("RSA", "EC");
-
- // Security.getProvider("BC") / Security.getProvider("BCFIPS").
- // also used to get Factories. e.g. CertificateFactory.getInstance("X.509", "BCFIPS")
- public static final String BC_FIPS = "BCFIPS";
- public static final String BC = "BC";
-
- public static boolean isBCFIPS() {
- return BC_PROVIDER.getClass().getCanonicalName().equals(BC_FIPS_PROVIDER_CLASS);
- }
-
- /**
- * Get Bouncy Castle provider, and call Security.addProvider(provider) if success.
- * 1. try get from classpath.
- * 2. try get from Nar.
- */
- public static Provider getProvider() {
- boolean isProviderInstalled =
- Security.getProvider(BC) != null || Security.getProvider(BC_FIPS) != null;
-
- if (isProviderInstalled) {
- Provider provider = Security.getProvider(BC) != null
- ? Security.getProvider(BC)
- : Security.getProvider(BC_FIPS);
- log.debug().attr("provider", provider.getName()).log("Already instantiated Bouncy Castle provider");
- return provider;
- }
-
- // Not installed, try load from class path
- try {
- return getBCProviderFromClassPath();
- } catch (Exception e) {
- log.warn().exception(e)
- .log("Not able to get Bouncy Castle provider for both FIPS and Non-FIPS from class path");
- throw new RuntimeException(e);
- }
- }
-
- private static Provider loadConscryptProvider() {
- Class> conscryptClazz;
-
- try {
- conscryptClazz = Class.forName("org.conscrypt.Conscrypt");
- conscryptClazz.getMethod("checkAvailability").invoke(null);
- } catch (Throwable e) {
- if (e instanceof ClassNotFoundException) {
- log.debug("Conscrypt isn't available in the classpath. Using JDK default security provider.");
- } else if (e.getCause() instanceof UnsatisfiedLinkError) {
- log.debug().attr("os", System.getProperty("os.name")).attr("arch", System.getProperty("os.arch"))
- .log("Conscrypt isn't available. Using JDK default security provider");
- } else {
- log.debug().attr("cause", e.getCause()).attr("reason", e.getMessage())
- .log("Conscrypt isn't available. Using JDK default security provider");
- }
- return null;
- }
-
- Provider provider;
- try {
- provider = (Provider) Class.forName(CONSCRYPT_PROVIDER_CLASS).getDeclaredConstructor().newInstance();
- } catch (ReflectiveOperationException e) {
- log.debug().attr("class", CONSCRYPT_PROVIDER_CLASS).exception(e)
- .log("Unable to get security provider");
- return null;
- }
-
- // Configure Conscrypt's default hostname verifier to use Pulsar's TlsHostnameVerifier which
- // is more relaxed than the Conscrypt HostnameVerifier checking for RFC 2818 conformity.
- //
- // Certificates used in Pulsar docs and examples aren't strictly RFC 2818 compliant since they use the
- // deprecated way of specifying the hostname in the CN field of the subject DN of the certificate.
- // RFC 2818 recommends the use of SAN (subjectAltName) extension for specifying the hostname in the dNSName
- // field of the subjectAltName extension.
- //
- // Conscrypt's default HostnameVerifier has dropped support for the deprecated method of specifying the hostname
- // in the CN field. Pulsar's TlsHostnameVerifier continues to support the CN field.
- //
- // more details of Conscrypt's hostname verification:
- // https://github.com/google/conscrypt/blob/master/IMPLEMENTATION_NOTES.md#hostname-verification
- //
- // Setting the default is sufficient on its own since Conscrypt 2.6.0: TrustManagerImpl used to ignore
- // the default verifier (https://github.com/google/conscrypt/issues/1015), which forced Pulsar to copy it
- // onto every TrustManager instance, but https://github.com/google/conscrypt/pull/1060 made it fall back
- // to the default, so that workaround has been removed.
- try {
- HostnameVerifier hostnameVerifier = new TlsHostnameVerifier();
- Object wrappedHostnameVerifier = conscryptClazz
- .getMethod("wrapHostnameVerifier",
- new Class>[]{HostnameVerifier.class}).invoke(null, hostnameVerifier);
- Method setDefaultHostnameVerifierMethod =
- conscryptClazz
- .getMethod("setDefaultHostnameVerifier",
- new Class>[]{Class.forName("org.conscrypt.ConscryptHostnameVerifier")});
- setDefaultHostnameVerifierMethod.invoke(null, wrappedHostnameVerifier);
- } catch (Exception e) {
- log.warn().exception(e).log("Unable to set default hostname verifier for Conscrypt");
- }
-
- Security.addProvider(provider);
- log.debug().attr("provider", provider.getName()).attr("class", CONSCRYPT_PROVIDER_CLASS)
- .log("Added security provider");
- return provider;
- }
-
- /**
- * Get Bouncy Castle provider from classpath, and call Security.addProvider.
- * Throw Exception if failed.
- */
- public static Provider getBCProviderFromClassPath() throws Exception {
- Class> clazz;
- try {
- // prefer non FIPS, for backward compatibility concern.
- clazz = Class.forName(BC_NON_FIPS_PROVIDER_CLASS);
- } catch (ClassNotFoundException cnf) {
- log.warn().attr("nonFipsClass", BC_NON_FIPS_PROVIDER_CLASS).attr("fipsClass", BC_FIPS_PROVIDER_CLASS)
- .log("Not able to get Bouncy Castle provider, try to get FIPS provider");
- // attempt to use the FIPS provider.
- clazz = Class.forName(BC_FIPS_PROVIDER_CLASS);
- }
-
- Provider provider = (Provider) clazz.getDeclaredConstructor().newInstance();
- Security.addProvider(provider);
- log.debug().attr("provider", provider.getName())
- .log("Found and Instantiated Bouncy Castle provider in classpath");
- return provider;
- }
-
- public static SSLContext createSslContext(boolean allowInsecureConnection, Certificate[] trustCertificates,
- String providerName)
- throws GeneralSecurityException {
- return createSslContext(allowInsecureConnection, trustCertificates, null, null, providerName);
- }
-
- public static SslContext createNettySslContextForClient(SslProvider sslProvider, boolean allowInsecureConnection,
- String trustCertsFilePath, Set ciphers,
- Set protocols)
- throws GeneralSecurityException, SSLException, FileNotFoundException, IOException {
- return createNettySslContextForClient(sslProvider, allowInsecureConnection, trustCertsFilePath,
- (Certificate[]) null,
- (PrivateKey) null, ciphers, protocols);
- }
-
- public static SSLContext createSslContext(boolean allowInsecureConnection, String trustCertsFilePath,
- String certFilePath, String keyFilePath, String providerName) throws GeneralSecurityException {
- X509Certificate[] trustCertificates = loadCertificatesFromPemFile(trustCertsFilePath);
- X509Certificate[] certificates = loadCertificatesFromPemFile(certFilePath);
- PrivateKey privateKey = loadPrivateKeyFromPemFile(keyFilePath);
- return createSslContext(allowInsecureConnection, trustCertificates, certificates, privateKey, providerName);
- }
-
- /**
- * Creates {@link SslContext} with capability to do auto-cert refresh.
- * @param allowInsecureConnection
- * @param trustCertsFilePath
- * @param certFilePath
- * @param keyFilePath
- * @param sslContextAlgorithm
- * @param refreshDurationSec
- * @param executor
- * @return
- * @throws GeneralSecurityException
- * @throws SSLException
- * @throws FileNotFoundException
- * @throws IOException
- */
- public static SslContext createAutoRefreshSslContextForClient(SslProvider sslProvider,
- boolean allowInsecureConnection,
- String trustCertsFilePath, String certFilePath,
- String keyFilePath, String sslContextAlgorithm,
- int refreshDurationSec,
- ScheduledExecutorService executor)
- throws GeneralSecurityException, SSLException, FileNotFoundException, IOException {
- KeyManagerProxy keyManager = new KeyManagerProxy(certFilePath, keyFilePath, refreshDurationSec, executor);
- SslContextBuilder sslContexBuilder = SslContextBuilder.forClient().sslProvider(sslProvider);
- sslContexBuilder.keyManager(keyManager);
- if (allowInsecureConnection) {
- sslContexBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE);
- } else {
- if (StringUtils.isNotBlank(trustCertsFilePath)) {
- TrustManagerProxy trustManager =
- new TrustManagerProxy(trustCertsFilePath, refreshDurationSec, executor);
- sslContexBuilder.trustManager(trustManager);
- }
- }
- return sslContexBuilder.build();
- }
-
- public static SslContext createNettySslContextForClient(SslProvider sslProvider, boolean allowInsecureConnection,
- String trustCertsFilePath,
- String certFilePath, String keyFilePath,
- Set ciphers,
- Set protocols)
- throws GeneralSecurityException, SSLException, FileNotFoundException, IOException {
- X509Certificate[] certificates = loadCertificatesFromPemFile(certFilePath);
- PrivateKey privateKey = loadPrivateKeyFromPemFile(keyFilePath);
- return createNettySslContextForClient(sslProvider, allowInsecureConnection, trustCertsFilePath, certificates,
- privateKey, ciphers, protocols);
- }
-
- public static SslContext createNettySslContextForClient(SslProvider sslProvider, boolean allowInsecureConnection,
- String trustCertsFilePath,
- Certificate[] certificates, PrivateKey privateKey,
- Set ciphers,
- Set protocols)
- throws GeneralSecurityException, SSLException, FileNotFoundException, IOException {
-
- if (StringUtils.isNotBlank(trustCertsFilePath)) {
- try (FileInputStream trustCertsStream = new FileInputStream(trustCertsFilePath)) {
- return createNettySslContextForClient(sslProvider, allowInsecureConnection, trustCertsStream,
- certificates,
- privateKey, ciphers, protocols);
- }
- } else {
- return createNettySslContextForClient(sslProvider, allowInsecureConnection, (InputStream) null,
- certificates,
- privateKey, ciphers, protocols);
- }
- }
-
- public static SslContext createNettySslContextForClient(SslProvider sslProvider, boolean allowInsecureConnection,
- InputStream trustCertsStream, Certificate[] certificates,
- PrivateKey privateKey, Set ciphers,
- Set protocols)
- throws GeneralSecurityException, SSLException, FileNotFoundException, IOException {
- SslContextBuilder builder = SslContextBuilder.forClient().sslProvider(sslProvider);
- setupTrustCerts(builder, allowInsecureConnection, trustCertsStream);
- setupKeyManager(builder, privateKey, (X509Certificate[]) certificates);
- setupCiphers(builder, ciphers);
- setupProtocols(builder, protocols);
- return builder.build();
- }
-
- public static SslContext createNettySslContextForServer(SslProvider sslProvider, boolean allowInsecureConnection,
- String trustCertsFilePath,
- String certFilePath, String keyFilePath,
- Set ciphers, Set protocols,
- boolean requireTrustedClientCertOnConnect)
- throws GeneralSecurityException, SSLException, FileNotFoundException, IOException {
- X509Certificate[] certificates = loadCertificatesFromPemFile(certFilePath);
- PrivateKey privateKey = loadPrivateKeyFromPemFile(keyFilePath);
-
- SslContextBuilder builder =
- SslContextBuilder.forServer(privateKey, certificates).sslProvider(sslProvider);
- setupCiphers(builder, ciphers);
- setupProtocols(builder, protocols);
- if (StringUtils.isNotBlank(trustCertsFilePath)) {
- try (FileInputStream trustCertsStream = new FileInputStream(trustCertsFilePath)) {
- setupTrustCerts(builder, allowInsecureConnection, trustCertsStream);
- }
- } else {
- setupTrustCerts(builder, allowInsecureConnection, null);
- }
- setupKeyManager(builder, privateKey, certificates);
- setupClientAuthentication(builder, requireTrustedClientCertOnConnect);
- return builder.build();
- }
-
- public static SSLContext createSslContext(boolean allowInsecureConnection, Certificate[] trustCertficates,
- Certificate[] certificates, PrivateKey privateKey)
- throws GeneralSecurityException {
- return createSslContext(allowInsecureConnection, trustCertficates, certificates, privateKey, null);
- }
-
- public static SSLContext createSslContext(boolean allowInsecureConnection, Certificate[] trustCertficates,
- Certificate[] certificates, PrivateKey privateKey, String providerName)
- throws GeneralSecurityException {
- KeyStoreHolder ksh = new KeyStoreHolder();
- TrustManager[] trustManagers = null;
- KeyManager[] keyManagers = null;
- Provider provider = resolveProvider(providerName);
-
- trustManagers = setupTrustCerts(ksh, allowInsecureConnection, trustCertficates, provider);
- keyManagers = setupKeyManager(ksh, privateKey, certificates);
-
- SSLContext sslCtx = provider != null ? SSLContext.getInstance("TLS", provider)
- : SSLContext.getInstance("TLS");
- sslCtx.init(keyManagers, trustManagers, new SecureRandom());
- return sslCtx;
- }
-
- private static KeyManager[] setupKeyManager(KeyStoreHolder ksh, PrivateKey privateKey, Certificate[] certificates)
- throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
- KeyManager[] keyManagers = null;
- if (certificates != null && privateKey != null) {
- ksh.setPrivateKey("private", privateKey, certificates);
- KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
- kmf.init(ksh.getKeyStore(), "".toCharArray());
- keyManagers = kmf.getKeyManagers();
- }
- return keyManagers;
- }
-
- private static TrustManager[] setupTrustCerts(KeyStoreHolder ksh, boolean allowInsecureConnection,
- Certificate[] trustCertficates, Provider securityProvider)
- throws NoSuchAlgorithmException, KeyStoreException {
- TrustManager[] trustManagers;
- if (allowInsecureConnection) {
- trustManagers = InsecureTrustManagerFactory.INSTANCE.getTrustManagers();
- } else {
- TrustManagerFactory tmf = securityProvider != null
- ? TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm(), securityProvider)
- : TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
-
- if (trustCertficates == null || trustCertficates.length == 0) {
- tmf.init((KeyStore) null);
- } else {
- for (int i = 0; i < trustCertficates.length; i++) {
- ksh.setCertificate("trust" + i, trustCertficates[i]);
- }
- tmf.init(ksh.getKeyStore());
- }
-
- trustManagers = tmf.getTrustManagers();
- }
- return trustManagers;
- }
-
- public static X509Certificate[] loadCertificatesFromPemFile(String certFilePath) throws KeyManagementException {
- X509Certificate[] certificates = null;
-
- if (certFilePath == null || certFilePath.isEmpty()) {
- return certificates;
- }
-
- try (FileInputStream input = new FileInputStream(certFilePath)) {
- certificates = loadCertificatesFromPemStream(input);
- } catch (GeneralSecurityException | IOException e) {
- throw new KeyManagementException("Certificate loading error", e);
- }
-
- return certificates;
- }
-
- public static X509Certificate[] loadCertificatesFromPemStream(InputStream inStream) throws KeyManagementException {
- if (inStream == null) {
- return null;
- }
- CertificateFactory cf;
- try {
- if (inStream.markSupported()) {
- inStream.reset();
- }
- cf = CertificateFactory.getInstance("X.509");
- @SuppressWarnings("unchecked") // CertificateFactory.getInstance("X.509") returns X509Certificate instances
- Collection collection = (Collection) cf.generateCertificates(inStream);
- return collection.toArray(new X509Certificate[collection.size()]);
- } catch (CertificateException | IOException e) {
- throw new KeyManagementException("Certificate loading error", e);
- }
- }
-
- public static PrivateKey loadPrivateKeyFromPemFile(String keyFilePath) throws KeyManagementException {
- if (keyFilePath == null || keyFilePath.isEmpty()) {
- return null;
- }
-
- PrivateKey privateKey;
-
- try (FileInputStream input = new FileInputStream(keyFilePath)) {
- privateKey = loadPrivateKeyFromPemStream(input);
- } catch (IOException e) {
- throw new KeyManagementException("Private key loading error", e);
- }
-
- return privateKey;
- }
-
- public static PrivateKey loadPrivateKeyFromPemStream(InputStream inStream) throws KeyManagementException {
- if (inStream == null) {
- return null;
- }
-
- PrivateKey privateKey;
-
- try (BufferedReader reader = new BufferedReader(new InputStreamReader(inStream, StandardCharsets.UTF_8))) {
- if (inStream.markSupported()) {
- inStream.reset();
- }
- StringBuilder sb = new StringBuilder();
- String currentLine = null;
-
- // Jump to the first line after -----BEGIN [RSA] PRIVATE KEY-----
- while ((currentLine = reader.readLine()) != null && !currentLine.startsWith("-----BEGIN")) {
- reader.readLine();
- }
-
- // Stop (and skip) at the last line that has, say, -----END [RSA] PRIVATE KEY-----
- while ((currentLine = reader.readLine()) != null && !currentLine.startsWith("-----END")) {
- sb.append(currentLine);
- }
- final KeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(sb.toString()));
- final List failedAlgorithm = new ArrayList<>(KEY_FACTORY_ALGORITHMS.size());
- for (String algorithm : KEY_FACTORY_ALGORITHMS) {
- try {
- return KeyFactory.getInstance(algorithm).generatePrivate(keySpec);
- } catch (InvalidKeySpecException | NoSuchAlgorithmException ex) {
- failedAlgorithm.add(algorithm);
- }
- }
- throw new KeyManagementException("The private key algorithm is not supported. attempted: "
- + StringUtils.join(failedAlgorithm, ","));
- } catch (IOException e) {
- throw new KeyManagementException("Private key loading error", e);
- }
-
- }
-
- private static void setupTrustCerts(SslContextBuilder builder, boolean allowInsecureConnection,
- InputStream trustCertsStream) throws IOException, FileNotFoundException {
- if (allowInsecureConnection) {
- builder.trustManager(InsecureTrustManagerFactory.INSTANCE);
- } else {
- if (trustCertsStream != null) {
- builder.trustManager(trustCertsStream);
- } else {
- builder.trustManager((File) null);
- }
- }
- }
-
- private static void setupKeyManager(SslContextBuilder builder, PrivateKey privateKey,
- X509Certificate[] certificates) {
- builder.keyManager(privateKey, certificates);
- }
-
- private static void setupCiphers(SslContextBuilder builder, Set ciphers) {
- if (ciphers != null && ciphers.size() > 0) {
- builder.ciphers(ciphers);
- }
- }
-
- private static void setupProtocols(SslContextBuilder builder, Set protocols) {
- if (protocols != null && protocols.size() > 0) {
- builder.protocols(protocols.toArray(new String[protocols.size()]));
- }
- }
-
- private static void setupClientAuthentication(SslContextBuilder builder,
- boolean requireTrustedClientCertOnConnect) {
- if (requireTrustedClientCertOnConnect) {
- builder.clientAuth(ClientAuth.REQUIRE);
- } else {
- builder.clientAuth(ClientAuth.OPTIONAL);
- }
- }
-
- public static void configureSSLHandler(SslHandler handler) {
- SSLEngine sslEngine = handler.engine();
- SSLParameters sslParameters = sslEngine.getSSLParameters();
- sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
- sslEngine.setSSLParameters(sslParameters);
- }
-
- public static Provider resolveProvider(String providerName) throws NoSuchAlgorithmException {
- Provider provider = null;
- if (!StringUtils.isEmpty(providerName)) {
- provider = Security.getProvider(providerName);
- }
-
- if (provider == null) {
- provider = SSLContext.getDefault().getProvider();
- }
-
- return provider;
- }
-
-}
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/util/TrustManagerProxy.java b/pulsar-common/src/main/java/org/apache/pulsar/common/util/TrustManagerProxy.java
deleted file mode 100644
index d913987a9e067..0000000000000
--- a/pulsar-common/src/main/java/org/apache/pulsar/common/util/TrustManagerProxy.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * 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 org.apache.pulsar.common.util;
-
-import io.netty.handler.ssl.SslContext;
-import java.io.IOException;
-import java.net.Socket;
-import java.security.KeyManagementException;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.cert.CertificateException;
-import java.security.cert.X509Certificate;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import javax.net.ssl.SSLEngine;
-import javax.net.ssl.TrustManagerFactory;
-import javax.net.ssl.X509ExtendedTrustManager;
-import lombok.CustomLog;
-
-/**
- * This class wraps {@link X509ExtendedTrustManager} and gives opportunity to refresh Trust-manager with refreshed certs
- * without changing {@link SslContext}.
- */
-@CustomLog
-public class TrustManagerProxy extends X509ExtendedTrustManager {
-
- private volatile X509ExtendedTrustManager trustManager;
- private final FileModifiedTimeUpdater certFile;
-
- public TrustManagerProxy(String caCertFile, int refreshDurationSec, ScheduledExecutorService executor) {
- this.certFile = new FileModifiedTimeUpdater(caCertFile);
- try {
- updateTrustManager();
- } catch (KeyManagementException | IOException | CertificateException e) {
- log.warn().attr("certFile", certFile).exceptionMessage(e).log("Failed to load cert");
- throw new IllegalArgumentException(e);
- } catch (NoSuchAlgorithmException | KeyStoreException e) {
- log.warn().exception(e).log("Failed to init trust-store");
- throw new IllegalArgumentException(e);
- }
- executor.scheduleWithFixedDelay(() -> updateTrustManagerSafely(), refreshDurationSec, refreshDurationSec,
- TimeUnit.SECONDS);
- }
-
- private void updateTrustManagerSafely() {
- try {
- updateTrustManager();
- } catch (Exception e) {
- log.warn().attr("certFile", certFile.getFileName()).exception(e).log("Failed to init trust-store");
- }
- }
-
- private void updateTrustManager() throws CertificateException, KeyStoreException, NoSuchAlgorithmException,
- IOException, KeyManagementException {
- KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
- keyStore.load(null);
- X509Certificate[] certificates = SecurityUtility.loadCertificatesFromPemFile(certFile.getFileName());
- for (X509Certificate certificate : certificates) {
- String alias = certificate.getSubjectX500Principal().getName();
- keyStore.setCertificateEntry(alias, certificate);
- }
- final TrustManagerFactory trustManagerFactory = TrustManagerFactory
- .getInstance(TrustManagerFactory.getDefaultAlgorithm());
- trustManagerFactory.init(keyStore);
- trustManager = (X509ExtendedTrustManager) trustManagerFactory.getTrustManagers()[0];
- }
-
- @Override
- public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
- trustManager.checkClientTrusted(x509Certificates, s);
- }
-
- @Override
- public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
- trustManager.checkServerTrusted(x509Certificates, s);
- }
-
- @Override
- public X509Certificate[] getAcceptedIssuers() {
- return trustManager.getAcceptedIssuers();
- }
-
- @Override
- public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket)
- throws CertificateException {
- trustManager.checkClientTrusted(chain, authType, socket);
- }
-
- @Override
- public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
- throws CertificateException {
- trustManager.checkClientTrusted(chain, authType, engine);
- }
-
- @Override
- public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket)
- throws CertificateException {
- trustManager.checkServerTrusted(chain, authType, socket);
- }
-
- @Override
- public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
- throws CertificateException {
- trustManager.checkServerTrusted(chain, authType, engine);
- }
-}
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/KeyStoreSSLContext.java b/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/KeyStoreSSLContext.java
deleted file mode 100644
index 1317e354fa9ac..0000000000000
--- a/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/KeyStoreSSLContext.java
+++ /dev/null
@@ -1,352 +0,0 @@
-/*
- * 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 org.apache.pulsar.common.util.keystoretls;
-
-import static org.apache.pulsar.common.util.SecurityUtility.getProvider;
-import com.google.common.base.Strings;
-import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.security.KeyStore;
-import java.security.Provider;
-import java.security.SecureRandom;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-import javax.net.ssl.KeyManager;
-import javax.net.ssl.KeyManagerFactory;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLEngine;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.TrustManagerFactory;
-import lombok.CustomLog;
-import lombok.Getter;
-import org.apache.pulsar.common.util.SecurityUtility;
-
-/**
- * KeyStoreSSLContext that mainly wrap a SSLContext to provide SSL context for both webservice and netty.
- */
-@CustomLog
-public class KeyStoreSSLContext {
- public static final String DEFAULT_KEYSTORE_TYPE = "JKS";
- public static final String DEFAULT_SSL_PROTOCOL = "TLS";
- public static final String DEFAULT_SSL_ENABLED_PROTOCOLS = "TLSv1.3,TLSv1.2";
- public static final String DEFAULT_SSL_KEYMANGER_ALGORITHM = KeyManagerFactory.getDefaultAlgorithm();
- public static final String DEFAULT_SSL_TRUSTMANAGER_ALGORITHM = TrustManagerFactory.getDefaultAlgorithm();
-
- public static final Provider BC_PROVIDER = getProvider();
-
- /**
- * Connection Mode for TLS.
- */
- public enum Mode {
- CLIENT,
- SERVER
- }
-
- @Getter
- private final Mode mode;
-
- private final String sslProviderString;
- private final String keyStoreTypeString;
- private final String keyStorePath;
- private final String keyStorePassword;
- private final boolean allowInsecureConnection;
- private final String trustStoreTypeString;
- private final String trustStorePath;
- private final String trustStorePassword;
- private final boolean needClientAuth;
- private final Set ciphers;
- private final Set protocols;
- private SSLContext sslContext;
-
- private final String protocol = DEFAULT_SSL_PROTOCOL;
- private final String kmfAlgorithm = DEFAULT_SSL_KEYMANGER_ALGORITHM;
- private final String tmfAlgorithm = DEFAULT_SSL_TRUSTMANAGER_ALGORITHM;
-
- // only init vars, before using it, need to call createSSLContext to create ssl context.
- public KeyStoreSSLContext(Mode mode,
- String sslProviderString,
- String keyStoreTypeString,
- String keyStorePath,
- String keyStorePassword,
- boolean allowInsecureConnection,
- String trustStoreTypeString,
- String trustStorePath,
- String trustStorePassword,
- boolean requireTrustedClientCertOnConnect,
- Set ciphers,
- Set protocols) {
- this.mode = mode;
- this.sslProviderString = sslProviderString;
- this.keyStoreTypeString = Strings.isNullOrEmpty(keyStoreTypeString)
- ? DEFAULT_KEYSTORE_TYPE
- : keyStoreTypeString;
- this.keyStorePath = keyStorePath;
- this.keyStorePassword = keyStorePassword;
- this.trustStoreTypeString = Strings.isNullOrEmpty(trustStoreTypeString)
- ? DEFAULT_KEYSTORE_TYPE
- : trustStoreTypeString;
- this.trustStorePath = trustStorePath;
- if (trustStorePassword == null) {
- this.trustStorePassword = "";
- } else {
- this.trustStorePassword = trustStorePassword;
- }
- this.needClientAuth = requireTrustedClientCertOnConnect;
-
- if (protocols != null && protocols.size() > 0) {
- this.protocols = protocols;
- } else {
- this.protocols = new HashSet<>(Arrays.asList(DEFAULT_SSL_ENABLED_PROTOCOLS.split("\\s*,\\s*")));
- }
-
- if (ciphers != null && ciphers.size() > 0) {
- this.ciphers = ciphers;
- } else {
- this.ciphers = null;
- }
-
- this.allowInsecureConnection = allowInsecureConnection;
- }
-
- public SSLContext createSSLContext() throws GeneralSecurityException, IOException {
- SSLContext sslContext;
-
- Provider provider = SecurityUtility.resolveProvider(sslProviderString);
- if (provider != null) {
- sslContext = SSLContext.getInstance(protocol, provider);
- } else {
- sslContext = SSLContext.getInstance(protocol);
- }
-
- // key store
- KeyManager[] keyManagers = null;
- if (!Strings.isNullOrEmpty(keyStorePath)) {
- KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(kmfAlgorithm);
- KeyStore keyStore = KeyStore.getInstance(keyStoreTypeString);
- char[] passwordChars = keyStorePassword.toCharArray();
- try (FileInputStream inputStream = new FileInputStream(keyStorePath)) {
- keyStore.load(inputStream, passwordChars);
- }
- keyManagerFactory.init(keyStore, passwordChars);
- keyManagers = keyManagerFactory.getKeyManagers();
- }
-
- // trust store
- TrustManagerFactory trustManagerFactory = null;
- if (this.allowInsecureConnection) {
- trustManagerFactory = InsecureTrustManagerFactory.INSTANCE;
- } else {
- if (!Strings.isNullOrEmpty(trustStorePath)) {
- trustManagerFactory = provider != null
- ? TrustManagerFactory.getInstance(tmfAlgorithm, provider)
- : TrustManagerFactory.getInstance(tmfAlgorithm);
- KeyStore trustStore = KeyStore.getInstance(trustStoreTypeString);
- char[] passwordChars = trustStorePassword.toCharArray();
- try (FileInputStream inputStream = new FileInputStream(trustStorePath)) {
- trustStore.load(inputStream, passwordChars);
- }
- trustManagerFactory.init(trustStore);
- }
- }
-
- TrustManager[] trustManagers = null;
- if (trustManagerFactory != null) {
- trustManagers = trustManagerFactory.getTrustManagers();
- }
-
- // init
- sslContext.init(keyManagers, trustManagers, new SecureRandom());
- this.sslContext = sslContext;
- return sslContext;
- }
-
- public SSLContext getSslContext() {
- if (sslContext == null) {
- throw new IllegalStateException("createSSLContext hasn't been called.");
- }
- return sslContext;
- }
-
- public SSLEngine createSSLEngine() {
- return configureSSLEngine(getSslContext().createSSLEngine());
- }
-
- public SSLEngine createSSLEngine(String peerHost, int peerPort) {
- return configureSSLEngine(getSslContext().createSSLEngine(peerHost, peerPort));
- }
-
- private SSLEngine configureSSLEngine(SSLEngine sslEngine) {
- sslEngine.setEnabledProtocols(protocols.toArray(new String[0]));
- if (this.ciphers != null) {
- sslEngine.setEnabledCipherSuites(this.ciphers.toArray(new String[0]));
- }
-
- if (this.mode == Mode.SERVER) {
- if (needClientAuth) {
- sslEngine.setNeedClientAuth(true);
- } else {
- sslEngine.setWantClientAuth(true);
- }
- sslEngine.setUseClientMode(false);
- } else {
- sslEngine.setUseClientMode(true);
- }
- return sslEngine;
- }
-
- public static KeyStoreSSLContext createClientKeyStoreSslContext(String sslProviderString,
- String keyStoreTypeString,
- String keyStorePath,
- String keyStorePassword,
- boolean allowInsecureConnection,
- String trustStoreTypeString,
- String trustStorePath,
- String trustStorePassword,
- Set ciphers,
- Set protocols)
- throws GeneralSecurityException, IOException {
- KeyStoreSSLContext keyStoreSSLContext = new KeyStoreSSLContext(Mode.CLIENT,
- sslProviderString,
- keyStoreTypeString,
- keyStorePath,
- keyStorePassword,
- allowInsecureConnection,
- trustStoreTypeString,
- trustStorePath,
- trustStorePassword,
- false,
- ciphers,
- protocols);
-
- keyStoreSSLContext.createSSLContext();
- return keyStoreSSLContext;
- }
-
-
- public static KeyStoreSSLContext createServerKeyStoreSslContext(String sslProviderString,
- String keyStoreTypeString,
- String keyStorePath,
- String keyStorePassword,
- boolean allowInsecureConnection,
- String trustStoreTypeString,
- String trustStorePath,
- String trustStorePassword,
- boolean requireTrustedClientCertOnConnect,
- Set ciphers,
- Set protocols)
- throws GeneralSecurityException, IOException {
- KeyStoreSSLContext keyStoreSSLContext = new KeyStoreSSLContext(Mode.SERVER,
- sslProviderString,
- keyStoreTypeString,
- keyStorePath,
- keyStorePassword,
- allowInsecureConnection,
- trustStoreTypeString,
- trustStorePath,
- trustStorePassword,
- requireTrustedClientCertOnConnect,
- ciphers,
- protocols);
-
- keyStoreSSLContext.createSSLContext();
- return keyStoreSSLContext;
- }
-
- // the web server only use this method to get SSLContext, it won't use this to configure engine
- // no need ciphers and protocols
- public static SSLContext createServerSslContext(String sslProviderString,
- String keyStoreTypeString,
- String keyStorePath,
- String keyStorePassword,
- boolean allowInsecureConnection,
- String trustStoreTypeString,
- String trustStorePath,
- String trustStorePassword,
- boolean requireTrustedClientCertOnConnect)
- throws GeneralSecurityException, IOException {
-
- return createServerKeyStoreSslContext(
- sslProviderString,
- keyStoreTypeString,
- keyStorePath,
- keyStorePassword,
- allowInsecureConnection,
- trustStoreTypeString,
- trustStorePath,
- trustStorePassword,
- requireTrustedClientCertOnConnect,
- null,
- null).getSslContext();
- }
-
- // for web client
- public static SSLContext createClientSslContext(String sslProviderString,
- String keyStoreTypeString,
- String keyStorePath,
- String keyStorePassword,
- boolean allowInsecureConnection,
- String trustStoreTypeString,
- String trustStorePath,
- String trustStorePassword,
- Set ciphers,
- Set protocol)
- throws GeneralSecurityException, IOException {
- KeyStoreSSLContext keyStoreSSLContext = new KeyStoreSSLContext(Mode.CLIENT,
- sslProviderString,
- keyStoreTypeString,
- keyStorePath,
- keyStorePassword,
- allowInsecureConnection,
- trustStoreTypeString,
- trustStorePath,
- trustStorePassword,
- false,
- ciphers,
- protocol);
-
- return keyStoreSSLContext.createSSLContext();
- }
-
- // for web client
- public static SSLContext createClientSslContext(String keyStoreTypeString,
- String keyStorePath,
- String keyStorePassword,
- String trustStoreTypeString,
- String trustStorePath,
- String trustStorePassword)
- throws GeneralSecurityException, IOException {
- KeyStoreSSLContext keyStoreSSLContext = new KeyStoreSSLContext(Mode.CLIENT,
- null,
- keyStoreTypeString,
- keyStorePath,
- keyStorePassword,
- false,
- trustStoreTypeString,
- trustStorePath,
- trustStorePassword,
- false,
- null,
- null);
-
- return keyStoreSSLContext.createSSLContext();
- }
-}
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/SSLContextValidatorEngine.java b/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/SSLContextValidatorEngine.java
deleted file mode 100644
index a69a18c21d97f..0000000000000
--- a/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/SSLContextValidatorEngine.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * 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 org.apache.pulsar.common.util.keystoretls;
-
-import static javax.net.ssl.SSLEngineResult.HandshakeStatus.FINISHED;
-import java.nio.ByteBuffer;
-import java.util.Arrays;
-import javax.net.ssl.SSLEngine;
-import javax.net.ssl.SSLEngineResult;
-import javax.net.ssl.SSLException;
-import lombok.CustomLog;
-
-/**
- * SSLContextValidatorEngine to validate 2 SSlContext.
- */
-@CustomLog
-public class SSLContextValidatorEngine {
- @FunctionalInterface
- public interface SSLEngineProvider {
- SSLEngine createSSLEngine(String peerHost, int peerPort);
- }
-
- private static final ByteBuffer EMPTY_BUF = ByteBuffer.allocate(0);
- private final SSLEngine sslEngine;
- private SSLEngineResult handshakeResult;
- private ByteBuffer appBuffer;
- private ByteBuffer netBuffer;
- private boolean finished = false;
-
- /**
- * Validates TLS handshake up to TLSv1.2.
- * TLSv1.3 has a differences in TLS handshake as described in https://stackoverflow.com/a/62465859
- */
- public static void validate(SSLEngineProvider clientSslEngineSupplier, SSLEngineProvider serverSslEngineSupplier)
- throws SSLException {
- SSLContextValidatorEngine clientEngine = new SSLContextValidatorEngine(clientSslEngineSupplier);
- if (Arrays.stream(clientEngine.sslEngine.getEnabledProtocols()).anyMatch(s -> s.equals("TLSv1.3"))) {
- throw new IllegalStateException("This validator doesn't support TLSv1.3");
- }
- SSLContextValidatorEngine serverEngine = new SSLContextValidatorEngine(serverSslEngineSupplier);
- try {
- clientEngine.beginHandshake();
- serverEngine.beginHandshake();
- while (!serverEngine.complete() || !clientEngine.complete()) {
- clientEngine.handshake(serverEngine);
- serverEngine.handshake(clientEngine);
- }
- } finally {
- clientEngine.close();
- serverEngine.close();
- }
- }
-
- private SSLContextValidatorEngine(SSLEngineProvider sslEngineSupplier) {
- this.sslEngine = sslEngineSupplier.createSSLEngine("localhost", 0);
- appBuffer = ByteBuffer.allocate(sslEngine.getSession().getApplicationBufferSize());
- netBuffer = ByteBuffer.allocate(sslEngine.getSession().getPacketBufferSize());
- }
-
- void beginHandshake() throws SSLException {
- sslEngine.beginHandshake();
- }
-
- void handshake(SSLContextValidatorEngine peerEngine) throws SSLException {
- SSLEngineResult.HandshakeStatus handshakeStatus = sslEngine.getHandshakeStatus();
- while (true) {
- switch (handshakeStatus) {
- case NEED_WRAP:
- handshakeResult = sslEngine.wrap(EMPTY_BUF, netBuffer);
- switch (handshakeResult.getStatus()) {
- case OK: break;
- case BUFFER_OVERFLOW:
- netBuffer.compact();
- netBuffer = ensureCapacity(netBuffer, sslEngine.getSession().getPacketBufferSize());
- netBuffer.flip();
- break;
- case BUFFER_UNDERFLOW:
- case CLOSED:
- default:
- throw new SSLException("Unexpected handshake status: " + handshakeResult.getStatus());
- }
- return;
- case NEED_UNWRAP:
- if (peerEngine.netBuffer.position() == 0) {
- return;
- }
- peerEngine.netBuffer.flip(); // unwrap the data from peer
- handshakeResult = sslEngine.unwrap(peerEngine.netBuffer, appBuffer);
- peerEngine.netBuffer.compact();
- handshakeStatus = handshakeResult.getHandshakeStatus();
- switch (handshakeResult.getStatus()) {
- case OK: break;
- case BUFFER_OVERFLOW:
- appBuffer = ensureCapacity(appBuffer, sslEngine.getSession().getApplicationBufferSize());
- break;
- case BUFFER_UNDERFLOW:
- netBuffer = ensureCapacity(netBuffer, sslEngine.getSession().getPacketBufferSize());
- break;
- case CLOSED:
- default:
- throw new SSLException("Unexpected handshake status: " + handshakeResult.getStatus());
- }
- break;
- case NEED_TASK:
- sslEngine.getDelegatedTask().run();
- handshakeStatus = sslEngine.getHandshakeStatus();
- break;
- case FINISHED:
- return;
- case NOT_HANDSHAKING:
- if (handshakeResult.getHandshakeStatus() != FINISHED) {
- throw new SSLException("Did not finish handshake");
- }
- finished = true;
- return;
- default:
- throw new IllegalStateException("Unexpected handshake status " + handshakeStatus);
- }
- }
- }
-
- boolean complete() {
- return finished;
- }
-
- void close() {
- sslEngine.closeOutbound();
- try {
- sslEngine.closeInbound();
- } catch (Exception e) {
- // ignore
- }
- }
-
- /**
- * Check if the given ByteBuffer capacity.
- * @param existingBuffer ByteBuffer capacity to check
- * @param newLength new length for the ByteBuffer.
- * returns ByteBuffer
- */
- public static ByteBuffer ensureCapacity(ByteBuffer existingBuffer, int newLength) {
- if (newLength > existingBuffer.capacity()) {
- ByteBuffer newBuffer = ByteBuffer.allocate(newLength);
- existingBuffer.flip();
- newBuffer.put(existingBuffer);
- return newBuffer;
- }
- return existingBuffer;
- }
-}
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/package-info.java b/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/package-info.java
deleted file mode 100644
index a9b27bb759501..0000000000000
--- a/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/package-info.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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.
- */
-
-/**
- * Helpers to work with events from the non-blocking I/O client-server framework.
- */
-package org.apache.pulsar.common.util.keystoretls;
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/util/tls/JcaProviders.java b/pulsar-common/src/main/java/org/apache/pulsar/common/util/tls/JcaProviders.java
index cffcc078a3df8..907dd235d8ce7 100644
--- a/pulsar-common/src/main/java/org/apache/pulsar/common/util/tls/JcaProviders.java
+++ b/pulsar-common/src/main/java/org/apache/pulsar/common/util/tls/JcaProviders.java
@@ -295,18 +295,21 @@ private static Provider loadConscryptProvider() {
}
// Unlike the PIP-337 loader this one installs NO custom hostname verifier, so Conscrypt keeps its
- // built-in default: standard RFC 2818 (SAN-based) verification, matching PIP-478's removal of the
- // deprecated CN-based matching. Since Conscrypt 2.6.0 a TrustManagerImpl falls back to the default
- // verifier on its own (https://github.com/google/conscrypt/pull/1060 fixing issue 1015), so nothing
- // has to be propagated onto the individual trust managers.
+ // built-in default: SAN-only verification, with no fallback to the CN. That is stricter than the
+ // fallback RFC 2818 section 3.1 mandates when a hostname is matched against a certificate carrying
+ // no dNSName SAN, and stricter than the last-resort CN-ID check RFC 6125 section 6.4.4 permitted —
+ // but it is what RFC 9525 (which obsoletes RFC 6125) now requires: "The Common Name RDN MUST NOT be
+ // used to identify a service". So it is the default engines that are lenient, not Conscrypt that is
+ // eccentric. (Neither fallback ever applied to an IP literal, which is matched against iPAddress
+ // SANs only, so pinning Conscrypt changes nothing there.) Since Conscrypt 2.6.0 a
+ // TrustManagerImpl falls back to the default verifier on its own
+ // (https://github.com/google/conscrypt/pull/1060 fixing issue 1015), so nothing has to be
+ // propagated onto the individual trust managers.
//
- // CAVEAT while both TLS stacks coexist: Conscrypt.setDefaultHostnameVerifier is process-global, and
- // SecurityUtility's static initializer (still the wired PIP-337 path) sets it to the CN-tolerant
- // TlsHostnameVerifier. Whenever that class has been loaded, that relaxed verifier — not Conscrypt's
- // own — is the default the trust managers fall back to. Nothing routes through this class yet so no
- // deployment is affected today; the PR that turns SAN-only hostname verification on by default must
- // neutralize that global (or land together with SecurityUtility's removal) for the SAN-only guarantee
- // to hold on a Conscrypt-pinned deployment.
+ // Nothing overrides that default any more: the CN-tolerant TlsHostnameVerifier that SecurityUtility
+ // used to install process-wide was removed with it. So a Conscrypt-pinned client rejects a CN-only
+ // server certificate that the JDK and OpenSSL engines still accept — the one deployment-visible
+ // change from removing Pulsar's own CN-matching verifier.
Security.addProvider(provider);
log.debug().attr("provider", provider.getName()).attr("class", CONSCRYPT_PROVIDER_CLASS)
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/util/tls/JdkSslContexts.java b/pulsar-common/src/main/java/org/apache/pulsar/common/util/tls/JdkSslContexts.java
index 93a0fa64096f0..b7b736700d838 100644
--- a/pulsar-common/src/main/java/org/apache/pulsar/common/util/tls/JdkSslContexts.java
+++ b/pulsar-common/src/main/java/org/apache/pulsar/common/util/tls/JdkSslContexts.java
@@ -273,8 +273,7 @@ public static KeyManagerFactory createKeyManagerFactory(PrivateKey privateKey, C
/**
* Build the {@link TrustManager}s for a set of trust anchors, honoring both provider axes: the carrier
* keystore holding the anchors comes from the pinned JCA provider and the {@code TrustManagerFactory} from
- * the pinned JSSE provider (algorithm-negotiated as on the key side). Also applies the Conscrypt
- * hostname-verifier propagation workaround.
+ * the pinned JSSE provider (algorithm-negotiated as on the key side).
*
* Exposed for the same reason as {@link #createKeyManagerFactory(PrivateKey, Certificate[], Provider,
* Provider)}: {@code SslContextBuilder.trustManager(X509Certificate...)} would have Netty build the
@@ -294,8 +293,9 @@ public static TrustManager[] createTrustManagers(Certificate[] trustCertificates
return InsecureTrustManagerFactory.INSTANCE.getTrustManagers();
}
// Same algorithm negotiation as the key-manager side: prefer the pinned provider's
- // TrustManagerFactory (BCJSSE registers PKIX, the platform default), fall back to the platform
- // factory for a provider that offers none (e.g. Conscrypt).
+ // TrustManagerFactory, falling back to the platform factory for a provider that registers none.
+ // Both providers Pulsar can pin do register one — BCJSSE and Conscrypt each offer PKIX — so the
+ // fallback exists for a third-party provider, not for either of them.
TrustManagerFactory tmf;
if (provider != null) {
String algorithm = supportedAlgorithm(provider, "TrustManagerFactory",
diff --git a/pulsar-common/src/test/java/org/apache/pulsar/common/util/DefaultPulsarSslFactoryTest.java b/pulsar-common/src/test/java/org/apache/pulsar/common/util/DefaultPulsarSslFactoryTest.java
deleted file mode 100644
index 010c57aef15f4..0000000000000
--- a/pulsar-common/src/test/java/org/apache/pulsar/common/util/DefaultPulsarSslFactoryTest.java
+++ /dev/null
@@ -1,282 +0,0 @@
-/*
- * 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 org.apache.pulsar.common.util;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertNotNull;
-import static org.testng.Assert.assertThrows;
-import com.google.common.io.Resources;
-import io.netty.buffer.ByteBufAllocator;
-import io.netty.handler.ssl.OpenSslEngine;
-import java.io.File;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-import javax.net.ssl.SSLEngine;
-import org.testng.annotations.Test;
-
-public class DefaultPulsarSslFactoryTest {
-
- public static final String KEYSTORE_FILE_PATH =
- getAbsolutePath("certificate-authority/jks/broker.keystore.jks");
- public static final String TRUSTSTORE_FILE_PATH =
- getAbsolutePath("certificate-authority/jks/broker.truststore.jks");
- public static final String TRUSTSTORE_NO_PASSWORD_FILE_PATH =
- getAbsolutePath("certificate-authority/jks/broker.truststore.nopassword.jks");
- public static final String KEYSTORE_PW = "111111";
- public static final String TRUSTSTORE_PW = "111111";
- public static final String KEYSTORE_TYPE = "JKS";
-
- public static final String CA_CERT_FILE_PATH =
- getAbsolutePath("certificate-authority/certs/ca.cert.pem");
- public static final String CERT_FILE_PATH =
- getAbsolutePath("certificate-authority/server-keys/broker.cert.pem");
- public static final String KEY_FILE_PATH =
- getAbsolutePath("certificate-authority/server-keys/broker.key-pk8.pem");
-
- @Test
- public void sslContextCreationUsingKeystoreTest() throws Exception {
- PulsarSslConfiguration pulsarSslConfiguration = PulsarSslConfiguration.builder()
- .tlsEnabledWithKeystore(true)
- .tlsKeyStoreType(KEYSTORE_TYPE)
- .tlsKeyStorePath(KEYSTORE_FILE_PATH)
- .tlsKeyStorePassword(KEYSTORE_PW)
- .tlsTrustStorePath(TRUSTSTORE_FILE_PATH)
- .tlsTrustStorePassword(TRUSTSTORE_PW)
- .build();
- PulsarSslFactory pulsarSslFactory = new DefaultPulsarSslFactory();
- pulsarSslFactory.initialize(pulsarSslConfiguration);
- pulsarSslFactory.createInternalSslContext();
- assertNotNull(pulsarSslFactory.getInternalSslContext());
- assertThrows(RuntimeException.class, pulsarSslFactory::getInternalNettySslContext);
- }
-
- @Test
- public void sslContextCreationUsingPasswordLessTruststoreTest() throws Exception {
- PulsarSslConfiguration pulsarSslConfiguration = PulsarSslConfiguration.builder()
- .tlsEnabledWithKeystore(true)
- .tlsKeyStoreType(KEYSTORE_TYPE)
- .tlsKeyStorePath(KEYSTORE_FILE_PATH)
- .tlsKeyStorePassword(KEYSTORE_PW)
- .tlsTrustStorePath(TRUSTSTORE_NO_PASSWORD_FILE_PATH)
- .build();
- PulsarSslFactory pulsarSslFactory = new DefaultPulsarSslFactory();
- pulsarSslFactory.initialize(pulsarSslConfiguration);
- pulsarSslFactory.createInternalSslContext();
- assertNotNull(pulsarSslFactory.getInternalSslContext());
- assertThrows(RuntimeException.class, pulsarSslFactory::getInternalNettySslContext);
- }
-
- @Test
- public void sslContextCreationUsingTlsCertsTest() throws Exception {
- PulsarSslConfiguration pulsarSslConfiguration = PulsarSslConfiguration.builder()
- .tlsCertificateFilePath(CERT_FILE_PATH)
- .tlsKeyFilePath(KEY_FILE_PATH)
- .tlsTrustCertsFilePath(CA_CERT_FILE_PATH)
- .build();
- PulsarSslFactory pulsarSslFactory = new DefaultPulsarSslFactory();
- pulsarSslFactory.initialize(pulsarSslConfiguration);
- pulsarSslFactory.createInternalSslContext();
- assertNotNull(pulsarSslFactory.getInternalNettySslContext());
- assertThrows(RuntimeException.class, pulsarSslFactory::getInternalSslContext);
- }
-
- @Test
- public void sslContextCreationUsingOnlyCACertsTest() throws Exception {
- PulsarSslConfiguration pulsarSslConfiguration = PulsarSslConfiguration.builder()
- .tlsTrustCertsFilePath(CA_CERT_FILE_PATH)
- .build();
- PulsarSslFactory pulsarSslFactory = new DefaultPulsarSslFactory();
- pulsarSslFactory.initialize(pulsarSslConfiguration);
- pulsarSslFactory.createInternalSslContext();
- assertNotNull(pulsarSslFactory.getInternalNettySslContext());
- assertThrows(RuntimeException.class, pulsarSslFactory::getInternalSslContext);
- }
-
- @Test
- public void sslContextCreationForWebClientConnections() throws Exception {
- PulsarSslConfiguration pulsarSslConfiguration = PulsarSslConfiguration.builder()
- .tlsCertificateFilePath(CERT_FILE_PATH)
- .tlsKeyFilePath(KEY_FILE_PATH)
- .tlsTrustCertsFilePath(CA_CERT_FILE_PATH)
- .isHttps(true)
- .build();
- PulsarSslFactory pulsarSslFactory = new DefaultPulsarSslFactory();
- pulsarSslFactory.initialize(pulsarSslConfiguration);
- pulsarSslFactory.createInternalSslContext();
- assertNotNull(pulsarSslFactory.getInternalSslContext());
- assertThrows(RuntimeException.class, pulsarSslFactory::getInternalNettySslContext);
- }
-
- @Test
- public void sslContextCreationForWebServerConnectionsTest() throws Exception {
- PulsarSslConfiguration pulsarSslConfiguration = PulsarSslConfiguration.builder()
- .tlsCertificateFilePath(CERT_FILE_PATH)
- .tlsKeyFilePath(KEY_FILE_PATH)
- .tlsTrustCertsFilePath(CA_CERT_FILE_PATH)
- .isHttps(true)
- .serverMode(true)
- .build();
- PulsarSslFactory pulsarSslFactory = new DefaultPulsarSslFactory();
- pulsarSslFactory.initialize(pulsarSslConfiguration);
- pulsarSslFactory.createInternalSslContext();
- assertNotNull(pulsarSslFactory.getInternalSslContext());
- assertThrows(RuntimeException.class, pulsarSslFactory::getInternalNettySslContext);
- }
-
- @Test
- public void sslEngineCreationWithEnabledProtocolsAndCiphersForOpenSSLTest() throws Exception {
- Set ciphers = new HashSet<>();
- ciphers.add("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
- ciphers.add("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256");
- Set protocols = new HashSet<>();
- protocols.add("TLSv1.2");
- protocols.add("TLSv1");
- PulsarSslConfiguration pulsarSslConfiguration = PulsarSslConfiguration.builder()
- .tlsCertificateFilePath(CERT_FILE_PATH)
- .tlsKeyFilePath(KEY_FILE_PATH)
- .tlsTrustCertsFilePath(CA_CERT_FILE_PATH)
- .tlsCiphers(ciphers)
- .tlsProtocols(protocols)
- .serverMode(true)
- .build();
- PulsarSslFactory pulsarSslFactory = new DefaultPulsarSslFactory();
- pulsarSslFactory.initialize(pulsarSslConfiguration);
- pulsarSslFactory.createInternalSslContext();
- assertNotNull(pulsarSslFactory.getInternalNettySslContext());
- assertThrows(RuntimeException.class, pulsarSslFactory::getInternalSslContext);
- SSLEngine sslEngine = pulsarSslFactory.createServerSslEngine(ByteBufAllocator.DEFAULT);
- /* Adding SSLv2Hello protocol only during expected checks as Netty adds it as part of the
- ReferenceCountedOpenSslEngine's setEnabledProtocols method. The reasoning is that OpenSSL currently has no
- way to disable this protocol.
- */
- protocols.add("SSLv2Hello");
- assertEquals(new HashSet<>(Arrays.asList(sslEngine.getEnabledProtocols())), protocols);
- assertEquals(new HashSet<>(Arrays.asList(sslEngine.getEnabledCipherSuites())), ciphers);
- assert(!sslEngine.getUseClientMode());
- }
-
- @Test
- public void sslEngineCreationWithEnabledProtocolsAndCiphersForWebTest() throws Exception {
- Set ciphers = new HashSet<>();
- ciphers.add("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
- ciphers.add("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256");
- Set protocols = new HashSet<>();
- protocols.add("TLSv1.2");
- protocols.add("TLSv1");
- PulsarSslConfiguration pulsarSslConfiguration = PulsarSslConfiguration.builder()
- .tlsCertificateFilePath(CERT_FILE_PATH)
- .tlsKeyFilePath(KEY_FILE_PATH)
- .tlsTrustCertsFilePath(CA_CERT_FILE_PATH)
- .tlsCiphers(ciphers)
- .tlsProtocols(protocols)
- .isHttps(true)
- .serverMode(true)
- .build();
- PulsarSslFactory pulsarSslFactory = new DefaultPulsarSslFactory();
- pulsarSslFactory.initialize(pulsarSslConfiguration);
- pulsarSslFactory.createInternalSslContext();
- assertNotNull(pulsarSslFactory.getInternalSslContext());
- assertThrows(RuntimeException.class, pulsarSslFactory::getInternalNettySslContext);
- SSLEngine sslEngine = pulsarSslFactory.createServerSslEngine(ByteBufAllocator.DEFAULT);
- assertEquals(new HashSet<>(Arrays.asList(sslEngine.getEnabledProtocols())), protocols);
- assertEquals(new HashSet<>(Arrays.asList(sslEngine.getEnabledCipherSuites())), ciphers);
- assert(!sslEngine.getUseClientMode());
- }
-
- @Test
- public void sslContextCreationAsOpenSslTlsProvider() throws Exception {
- PulsarSslConfiguration pulsarSslConfiguration = PulsarSslConfiguration.builder()
- .tlsProvider("OPENSSL")
- .tlsCertificateFilePath(CERT_FILE_PATH)
- .tlsKeyFilePath(KEY_FILE_PATH)
- .tlsTrustCertsFilePath(CA_CERT_FILE_PATH)
- .serverMode(true)
- .build();
- PulsarSslFactory pulsarSslFactory = new DefaultPulsarSslFactory();
- pulsarSslFactory.initialize(pulsarSslConfiguration);
- pulsarSslFactory.createInternalSslContext();
- assertNotNull(pulsarSslFactory.getInternalNettySslContext());
- assertThrows(RuntimeException.class, pulsarSslFactory::getInternalSslContext);
- SSLEngine sslEngine = pulsarSslFactory.createServerSslEngine(ByteBufAllocator.DEFAULT);
- assert(sslEngine instanceof OpenSslEngine);
- }
-
- @Test
- public void sslContextCreationAsJDKTlsProvider() throws Exception {
- PulsarSslConfiguration pulsarSslConfiguration = PulsarSslConfiguration.builder()
- .tlsProvider("JDK")
- .tlsCertificateFilePath(CERT_FILE_PATH)
- .tlsKeyFilePath(KEY_FILE_PATH)
- .tlsTrustCertsFilePath(CA_CERT_FILE_PATH)
- .serverMode(true)
- .build();
- PulsarSslFactory pulsarSslFactory = new DefaultPulsarSslFactory();
- pulsarSslFactory.initialize(pulsarSslConfiguration);
- pulsarSslFactory.createInternalSslContext();
- assertNotNull(pulsarSslFactory.getInternalNettySslContext());
- assertThrows(RuntimeException.class, pulsarSslFactory::getInternalSslContext);
- SSLEngine sslEngine = pulsarSslFactory.createServerSslEngine(ByteBufAllocator.DEFAULT);
- assert (!(sslEngine instanceof OpenSslEngine));
- }
-
- @Test
- public void sslEngineMutualAuthEnabledTest() throws Exception {
- PulsarSslConfiguration pulsarSslConfiguration = PulsarSslConfiguration.builder()
- .tlsProvider("JDK")
- .tlsCertificateFilePath(CERT_FILE_PATH)
- .tlsKeyFilePath(KEY_FILE_PATH)
- .tlsTrustCertsFilePath(CA_CERT_FILE_PATH)
- .requireTrustedClientCertOnConnect(true)
- .serverMode(true)
- .build();
- PulsarSslFactory pulsarSslFactory = new DefaultPulsarSslFactory();
- pulsarSslFactory.initialize(pulsarSslConfiguration);
- pulsarSslFactory.createInternalSslContext();
- assertNotNull(pulsarSslFactory.getInternalNettySslContext());
- assertThrows(RuntimeException.class, pulsarSslFactory::getInternalSslContext);
- SSLEngine sslEngine = pulsarSslFactory.createServerSslEngine(ByteBufAllocator.DEFAULT);
- assert(sslEngine.getNeedClientAuth());
- }
-
- @Test
- public void sslEngineSniClientTest() throws Exception {
- PulsarSslConfiguration pulsarSslConfiguration = PulsarSslConfiguration.builder()
- .tlsCertificateFilePath(CERT_FILE_PATH)
- .tlsKeyFilePath(KEY_FILE_PATH)
- .tlsTrustCertsFilePath(CA_CERT_FILE_PATH)
- .build();
- PulsarSslFactory pulsarSslFactory = new DefaultPulsarSslFactory();
- pulsarSslFactory.initialize(pulsarSslConfiguration);
- pulsarSslFactory.createInternalSslContext();
- assertNotNull(pulsarSslFactory.getInternalNettySslContext());
- assertThrows(RuntimeException.class, pulsarSslFactory::getInternalSslContext);
- SSLEngine sslEngine = pulsarSslFactory.createClientSslEngine(ByteBufAllocator.DEFAULT, "localhost",
- 1234);
- assertEquals(sslEngine.getPeerHost(), "localhost");
- assertEquals(sslEngine.getPeerPort(), 1234);
- }
-
-
-
- private static String getAbsolutePath(String resourceName) {
- return new File(Resources.getResource(resourceName).getPath()).getAbsolutePath();
- }
-
-}
diff --git a/pulsar-common/src/test/java/org/apache/pulsar/common/util/KeyManagerProxyTest.java b/pulsar-common/src/test/java/org/apache/pulsar/common/util/KeyManagerProxyTest.java
deleted file mode 100644
index 5542f0b22ac95..0000000000000
--- a/pulsar-common/src/test/java/org/apache/pulsar/common/util/KeyManagerProxyTest.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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 org.apache.pulsar.common.util;
-
-import static org.testng.Assert.assertEquals;
-import com.google.common.io.Resources;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import lombok.Cleanup;
-import org.testng.annotations.DataProvider;
-import org.testng.annotations.Test;
-
-public class KeyManagerProxyTest {
-
- @DataProvider(name = "certDataProvider")
- public static Object[][] caDataProvider() {
- return new Object[][]{
- {"ca/multiple-ca.pem", 2},
- {"ca/single-ca.pem", 1}
- };
- }
-
- @Test(dataProvider = "certDataProvider")
- public void testLoadCert(String path, int certCount) {
- final String certFilePath = Resources.getResource(path).getPath();
- // This key is not paired with certs, but this is not a problem as the key is not used in this test
- final String keyFilePath = Resources.getResource("ssl/my-ca/client-key.pem").getPath();
- @Cleanup("shutdownNow")
- final ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
-
- final KeyManagerProxy keyManager = new KeyManagerProxy(certFilePath, keyFilePath, 60, scheduledExecutor);
- assertEquals(keyManager.getCertificateChain("cn=test1").length, certCount);
- }
-}
diff --git a/pulsar-common/src/test/java/org/apache/pulsar/common/util/TrustManagerProxyTest.java b/pulsar-common/src/test/java/org/apache/pulsar/common/util/TrustManagerProxyTest.java
deleted file mode 100644
index ab31740bd5f11..0000000000000
--- a/pulsar-common/src/test/java/org/apache/pulsar/common/util/TrustManagerProxyTest.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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 org.apache.pulsar.common.util;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertNotNull;
-import com.google.common.io.Resources;
-import java.security.cert.X509Certificate;
-import java.util.Arrays;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import lombok.Cleanup;
-import org.testng.annotations.DataProvider;
-import org.testng.annotations.Test;
-
-public class TrustManagerProxyTest {
- @DataProvider(name = "caDataProvider")
- public static Object[][] caDataProvider() {
- return new Object[][]{
- {"ca/multiple-ca.pem", 2},
- {"ca/single-ca.pem", 1}
- };
- }
-
- @Test(dataProvider = "caDataProvider")
- public void testLoadCA(String path, int count) {
- String caPath = Resources.getResource(path).getPath();
-
- @Cleanup("shutdownNow")
- ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
- TrustManagerProxy trustManagerProxy =
- new TrustManagerProxy(caPath, 120, scheduledExecutor);
- X509Certificate[] x509Certificates = trustManagerProxy.getAcceptedIssuers();
- assertNotNull(x509Certificates);
- assertEquals(Arrays.stream(x509Certificates).count(), count);
- }
-}
diff --git a/pulsar-common/src/test/java/org/apache/pulsar/common/util/netty/SslContextTest.java b/pulsar-common/src/test/java/org/apache/pulsar/common/util/netty/SslContextTest.java
deleted file mode 100644
index 8dc9b1de95aea..0000000000000
--- a/pulsar-common/src/test/java/org/apache/pulsar/common/util/netty/SslContextTest.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * 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 org.apache.pulsar.common.util.netty;
-
-import static org.testng.Assert.assertThrows;
-import com.google.common.io.Resources;
-import io.netty.handler.ssl.SslProvider;
-import java.util.HashSet;
-import java.util.Set;
-import javax.net.ssl.SSLException;
-import org.apache.pulsar.client.api.AuthenticationDataProvider;
-import org.apache.pulsar.client.api.KeyStoreParams;
-import org.apache.pulsar.common.util.DefaultPulsarSslFactory;
-import org.apache.pulsar.common.util.PulsarSslConfiguration;
-import org.apache.pulsar.common.util.PulsarSslFactory;
-import org.testng.annotations.DataProvider;
-import org.testng.annotations.Test;
-
-public class SslContextTest {
- static final String BROKER_KEY_STORE_PATH =
- Resources.getResource("certificate-authority/jks/broker.keystore.jks").getPath();
- static final String BROKER_TRUST_STORE_PATH =
- Resources.getResource("certificate-authority/jks/broker.truststore.jks").getPath();
- static final String KEY_STORE_TYPE = "JKS";
- static final String KEY_STORE_PASSWORD = "111111";
-
- static final String CA_CERT_PATH = Resources.getResource("certificate-authority/certs/ca.cert.pem").getPath();
- static final String BROKER_CERT_PATH =
- Resources.getResource("certificate-authority/server-keys/broker.cert.pem").getPath();
- static final String BROKER_KEY_PATH =
- Resources.getResource("certificate-authority/server-keys/broker.key-pk8.pem").getPath();
-
- @DataProvider(name = "caCertSslContextDataProvider")
- public static Object[][] getSslContextDataProvider() {
- Set ciphers = new HashSet<>();
- ciphers.add("TLS_DHE_RSA_WITH_AES_256_GCM_SHA384");
- ciphers.add("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
- ciphers.add("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256");
- ciphers.add("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384");
- ciphers.add("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384");
-
- // Note: OPENSSL doesn't support these ciphers.
- return new Object[][]{
- new Object[]{SslProvider.JDK, ciphers},
- new Object[]{SslProvider.JDK, null},
-
- new Object[]{SslProvider.OPENSSL, ciphers},
- new Object[]{SslProvider.OPENSSL, null},
-
- new Object[]{null, ciphers},
- new Object[]{null, null},
- };
- }
-
- @DataProvider(name = "cipherDataProvider")
- public static Object[] getCipher() {
- Set cipher = new HashSet<>();
- cipher.add("TLS_DHE_RSA_WITH_AES_256_GCM_SHA384");
- cipher.add("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
- cipher.add("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256");
- cipher.add("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384");
- cipher.add("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384");
-
- return new Object[]{null, cipher};
- }
-
- @Test(dataProvider = "cipherDataProvider")
- @SuppressWarnings("try")
- public void testServerKeyStoreSSLContext(Set cipher) throws Exception {
- PulsarSslConfiguration pulsarSslConfiguration = PulsarSslConfiguration.builder()
- .tlsEnabledWithKeystore(true)
- .tlsKeyStoreType(KEY_STORE_TYPE)
- .tlsKeyStorePath(BROKER_KEY_STORE_PATH)
- .tlsKeyStorePassword(KEY_STORE_PASSWORD)
- .allowInsecureConnection(false)
- .tlsTrustStoreType(KEY_STORE_TYPE)
- .tlsTrustStorePath(BROKER_TRUST_STORE_PATH)
- .tlsTrustStorePassword(KEY_STORE_PASSWORD)
- .requireTrustedClientCertOnConnect(true)
- .tlsCiphers(cipher)
- .build();
- try (PulsarSslFactory pulsarSslFactory = new DefaultPulsarSslFactory()) {
- pulsarSslFactory.initialize(pulsarSslConfiguration);
- pulsarSslFactory.createInternalSslContext();
- }
- }
-
- private static class ClientAuthenticationData implements AuthenticationDataProvider {
- @Override
- public KeyStoreParams getTlsKeyStoreParams() {
- return null;
- }
- }
-
- @Test(dataProvider = "cipherDataProvider")
- @SuppressWarnings("try")
- public void testClientKeyStoreSSLContext(Set cipher) throws Exception {
- PulsarSslConfiguration pulsarSslConfiguration = PulsarSslConfiguration.builder()
- .allowInsecureConnection(false)
- .tlsEnabledWithKeystore(true)
- .tlsTrustStoreType(KEY_STORE_TYPE)
- .tlsTrustStorePath(BROKER_TRUST_STORE_PATH)
- .tlsTrustStorePassword(KEY_STORE_PASSWORD)
- .tlsCiphers(cipher)
- .authData(new ClientAuthenticationData())
- .build();
- try (PulsarSslFactory pulsarSslFactory = new DefaultPulsarSslFactory()) {
- pulsarSslFactory.initialize(pulsarSslConfiguration);
- pulsarSslFactory.createInternalSslContext();
- }
- }
-
- @Test(dataProvider = "caCertSslContextDataProvider")
- @SuppressWarnings("try")
- public void testServerCaCertSslContextWithSslProvider(SslProvider sslProvider, Set ciphers)
- throws Exception {
- try (PulsarSslFactory pulsarSslFactory = new DefaultPulsarSslFactory()) {
- PulsarSslConfiguration.PulsarSslConfigurationBuilder builder = PulsarSslConfiguration.builder()
- .tlsTrustCertsFilePath(CA_CERT_PATH)
- .tlsCertificateFilePath(BROKER_CERT_PATH)
- .tlsKeyFilePath(BROKER_KEY_PATH)
- .tlsCiphers(ciphers)
- .requireTrustedClientCertOnConnect(true);
- if (sslProvider != null) {
- builder.tlsProvider(sslProvider.name());
- }
- PulsarSslConfiguration pulsarSslConfiguration = builder.build();
- pulsarSslFactory.initialize(pulsarSslConfiguration);
-
- if (ciphers != null) {
- if (sslProvider == null || sslProvider == SslProvider.OPENSSL) {
- assertThrows(SSLException.class, pulsarSslFactory::createInternalSslContext);
- return;
- }
- }
- pulsarSslFactory.createInternalSslContext();
- }
- }
-
- @Test(dataProvider = "caCertSslContextDataProvider")
- @SuppressWarnings("try")
- public void testClientCaCertSslContextWithSslProvider(SslProvider sslProvider, Set ciphers)
- throws Exception {
- try (PulsarSslFactory pulsarSslFactory = new DefaultPulsarSslFactory()) {
- PulsarSslConfiguration.PulsarSslConfigurationBuilder builder = PulsarSslConfiguration.builder()
- .allowInsecureConnection(true)
- .tlsTrustCertsFilePath(CA_CERT_PATH)
- .tlsCiphers(ciphers);
- if (sslProvider != null) {
- builder.tlsProvider(sslProvider.name());
- }
- PulsarSslConfiguration pulsarSslConfiguration = builder.build();
- pulsarSslFactory.initialize(pulsarSslConfiguration);
- if (ciphers != null) {
- if (sslProvider == null || sslProvider == SslProvider.OPENSSL) {
- assertThrows(SSLException.class, pulsarSslFactory::createInternalSslContext);
- return;
- }
- }
- pulsarSslFactory.createInternalSslContext();
- }
- }
-}
diff --git a/pulsar-common/src/test/java/org/apache/pulsar/common/util/tls/JdkSslContextsTest.java b/pulsar-common/src/test/java/org/apache/pulsar/common/util/tls/JdkSslContextsTest.java
index c47c84c8eb313..74e54b003c7c0 100644
--- a/pulsar-common/src/test/java/org/apache/pulsar/common/util/tls/JdkSslContextsTest.java
+++ b/pulsar-common/src/test/java/org/apache/pulsar/common/util/tls/JdkSslContextsTest.java
@@ -99,14 +99,16 @@ public void algorithmNegotiationSelectsTheAliasedFallbackForBcjsseShapedProvider
.isEqualTo("PKIX");
}
- // A JSSE provider with no key/trust-manager services at all (Conscrypt) selects no provider algorithm;
+ // A JSSE provider registering no key/trust-manager services selects no provider algorithm; real
+ // Conscrypt is not such a provider (it registers PKIX for both), so this stub stands in for a
+ // third-party one;
// the caller then falls back to the platform default factory while the SSLContext stays pinned.
@Test
public void algorithmNegotiationReturnsNullForProvidersWithoutTheService() {
- Provider conscryptShaped = new Provider("Conscrypt-shaped", "1.0", "test stub") { };
- assertThat(JdkSslContexts.supportedAlgorithm(conscryptShaped, "KeyManagerFactory", "SunX509", "PKIX"))
+ Provider noFactoryProvider = new Provider("no-factory-provider", "1.0", "test stub") { };
+ assertThat(JdkSslContexts.supportedAlgorithm(noFactoryProvider, "KeyManagerFactory", "SunX509", "PKIX"))
.isNull();
- assertThat(JdkSslContexts.supportedAlgorithm(conscryptShaped, "TrustManagerFactory", "PKIX", "PKIX"))
+ assertThat(JdkSslContexts.supportedAlgorithm(noFactoryProvider, "TrustManagerFactory", "PKIX", "PKIX"))
.isNull();
}
diff --git a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyKeyStoreTlsTransportTest.java b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyKeyStoreTlsTransportTest.java
index c72be85812a3c..4c9d106d0bc0a 100644
--- a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyKeyStoreTlsTransportTest.java
+++ b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyKeyStoreTlsTransportTest.java
@@ -70,7 +70,7 @@ protected void setup() throws Exception {
proxyConfig.setWebServicePort(Optional.of(0));
proxyConfig.setWebServicePortTls(Optional.of(0));
// Advertise over loopback so the proxy service URL host (localhost) matches the keystore server
- // certificate's SubjectAltName. TLS hostname verification is on by default (PIP-478, SAN-only),
+ // certificate's SubjectAltName. TLS hostname verification is on by default (PIP-478),
// and the default advertised address resolves to the machine's canonical hostname, which is not
// in the cert SAN. This keeps hostname verification genuinely enabled. (The broker already
// advertises localhost via the base test config, so the proxy->broker TLS connection matches too.)
diff --git a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyKeyStoreTlsWithAuthTest.java b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyKeyStoreTlsWithAuthTest.java
index d081c64e167d3..8af625572cc9a 100644
--- a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyKeyStoreTlsWithAuthTest.java
+++ b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyKeyStoreTlsWithAuthTest.java
@@ -68,7 +68,7 @@ protected void setup() throws Exception {
proxyConfig.setWebServicePort(Optional.of(0));
proxyConfig.setWebServicePortTls(Optional.of(0));
// Advertise over loopback so the proxy service URL host (localhost) matches the keystore server
- // certificate's SubjectAltName. TLS hostname verification is on by default (PIP-478, SAN-only),
+ // certificate's SubjectAltName. TLS hostname verification is on by default (PIP-478),
// and the default advertised address resolves to the machine's canonical hostname, which is not
// in the cert SAN. This keeps hostname verification genuinely enabled.
proxyConfig.setAdvertisedAddress("localhost");
diff --git a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyKeyStoreTlsWithoutAuthTest.java b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyKeyStoreTlsWithoutAuthTest.java
index a5dbab28f3607..7cb67ed0ecf2e 100644
--- a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyKeyStoreTlsWithoutAuthTest.java
+++ b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyKeyStoreTlsWithoutAuthTest.java
@@ -64,7 +64,7 @@ protected void setup() throws Exception {
proxyConfig.setWebServicePort(Optional.of(0));
proxyConfig.setWebServicePortTls(Optional.of(0));
// Advertise over loopback so the proxy service URL host (localhost) matches the keystore server
- // certificate's SubjectAltName. TLS hostname verification is on by default (PIP-478, SAN-only),
+ // certificate's SubjectAltName. TLS hostname verification is on by default (PIP-478),
// and the default advertised address resolves to the machine's canonical hostname, which is not
// in the cert SAN. This keeps hostname verification genuinely enabled.
proxyConfig.setAdvertisedAddress("localhost");
diff --git a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyMutualTlsTest.java b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyMutualTlsTest.java
index 41f1cd832ca13..7fa9a34e54511 100644
--- a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyMutualTlsTest.java
+++ b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyMutualTlsTest.java
@@ -65,7 +65,7 @@ protected void setup() throws Exception {
proxyConfig.setWebServicePortTls(Optional.of(0));
// Advertise over loopback so the proxy service URL host (localhost) matches the server
// certificate's SubjectAltName (DNS:localhost, IP:127.0.0.1). TLS hostname verification is on by
- // default (PIP-478, SAN-only), and the default advertised address resolves to the machine's
+ // default (PIP-478), and the default advertised address resolves to the machine's
// canonical hostname/IP, which is not in the cert SAN. This keeps hostname verification enabled.
proxyConfig.setAdvertisedAddress("localhost");
proxyConfig.setTlsCertificateFilePath(TLS_PROXY_CERT_FILE_PATH);
diff --git a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyServiceTlsStarterTest.java b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyServiceTlsStarterTest.java
index c50bca3e0cd84..87714f96ae680 100644
--- a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyServiceTlsStarterTest.java
+++ b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyServiceTlsStarterTest.java
@@ -78,7 +78,7 @@ protected void setup() throws Exception {
serviceStarter.getConfig().setClusterName(configClusterName);
// Advertise over loopback so the proxy service URL host (localhost) matches the proxy server
// certificate's SubjectAltName (proxy.cert.pem carries DNS:localhost, IP:127.0.0.1). TLS hostname
- // verification is on by default (PIP-478, SAN-only), and the default advertised address (loaded from
+ // verification is on by default (PIP-478), and the default advertised address (loaded from
// conf/proxy.conf) resolves to the machine's canonical hostname/IP, which is not in the cert SAN.
serviceStarter.getConfig().setAdvertisedAddress("localhost");
serviceStarter.start();
diff --git a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyTlsFactoryTest.java b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyTlsFactoryTest.java
index 64f4b4f459c02..cdeea22b0a25d 100644
--- a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyTlsFactoryTest.java
+++ b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyTlsFactoryTest.java
@@ -61,7 +61,7 @@ protected void setup() throws Exception {
proxyConfig.setWebServicePortTls(Optional.of(0));
// Advertise over loopback so the proxy service URL host (localhost) matches the proxy server
// certificate's SubjectAltName (proxy.cert.pem carries DNS:localhost, IP:127.0.0.1). TLS hostname
- // verification is on by default (PIP-478, SAN-only), and the default advertised address resolves
+ // verification is on by default (PIP-478), and the default advertised address resolves
// to the machine's canonical hostname/IP, which is not in the cert SAN. Keeps verification enabled.
proxyConfig.setAdvertisedAddress("localhost");
proxyConfig.setTlsEnabledWithBroker(false);
diff --git a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyTlsTest.java b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyTlsTest.java
index d406016141a27..887f3001d7463 100644
--- a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyTlsTest.java
+++ b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyTlsTest.java
@@ -59,7 +59,7 @@ protected void setup() throws Exception {
proxyConfig.setWebServicePortTls(Optional.of(0));
// Advertise over loopback so the proxy service URL host (localhost) matches the proxy server
// certificate's SubjectAltName (proxy.cert.pem carries DNS:localhost, IP:127.0.0.1). TLS hostname
- // verification is on by default (PIP-478, SAN-only), and the default advertised address resolves
+ // verification is on by default (PIP-478), and the default advertised address resolves
// to the machine's canonical hostname/IP, which is not in the cert SAN. Keeps verification enabled.
proxyConfig.setAdvertisedAddress("localhost");
proxyConfig.setTlsEnabledWithBroker(false);
diff --git a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyWithAuthorizationTest.java b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyWithAuthorizationTest.java
index 23925a430e683..ba90173b01714 100644
--- a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyWithAuthorizationTest.java
+++ b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyWithAuthorizationTest.java
@@ -73,10 +73,11 @@ public class ProxyWithAuthorizationTest extends ProducerConsumerBase {
// The Proxy, Client, and SuperUser Client certs are signed by this CA
private static final String TLS_TRUST_CERT_FILE_PATH = "./src/test/resources/authentication/tls/cacert.pem";
- // A valid cert (signed by the trusted CA) that has NO Subject Alternative Name. Used only by the two
- // dedicated hostname-verification tests to prove that SAN-based (RFC 2818) hostname verification correctly
- // rejects a connection whose server certificate does not match the host. CN-based matching is no longer
- // supported (PIP-478, Pulsar 5.0), so this cert always fails hostname verification.
+ // A valid cert (signed by the trusted CA) that has NO Subject Alternative Name and CN=Broker. Used only
+ // by the two dedicated hostname-verification tests to prove that hostname verification correctly rejects
+ // a connection whose server certificate does not match the host. It fails on both counts: there is no SAN
+ // to match, and the CN that the JDK and OpenSSL engines fall back to for a SAN-less certificate is
+ // "Broker", not the advertised "localhost".
private static final String TLS_NO_SUBJECT_CERT_FILE_PATH =
"./src/test/resources/authentication/tls/ProxyWithAuthorizationTest/no-subject-alt-cert.pem";
private static final String TLS_NO_SUBJECT_KEY_FILE_PATH =
@@ -186,7 +187,7 @@ protected void doInitConf() throws Exception {
conf.setProxyRoles(Collections.singleton("Proxy"));
// Advertise over loopback so the broker service/web URLs the proxy connects to (localhost) match the
// broker server certificate's SubjectAltName (tls/server-cert.pem carries DNS:localhost, IP:127.0.0.1).
- // TLS hostname verification is on by default (PIP-478, SAN-only), and the default advertised address
+ // TLS hostname verification is on by default (PIP-478), and the default advertised address
// resolves to the machine's canonical hostname, which is not in the cert SAN (fails on CI runners).
conf.setAdvertisedAddress("localhost");
@@ -245,10 +246,10 @@ protected void setup() throws Exception {
proxyConfig.setBrokerWebServiceURLTLS(pulsar.getWebServiceAddressTls());
// Advertise over loopback so the client-facing proxy service URL host (localhost) matches the proxy
// server certificate's SubjectAltName (tls/server-cert.pem carries DNS:localhost, IP:127.0.0.1). TLS
- // hostname verification is on by default (PIP-478, SAN-only); the default advertised address resolves
+ // hostname verification is on by default (PIP-478); the default advertised address resolves
// to the machine's canonical hostname, which is not in the cert SAN (fails on CI runners). The two
- // dedicated hostname-verification tests still fail as intended because their server presents a no-SAN
- // certificate, which cannot match any host regardless of the advertised address.
+ // dedicated hostname-verification tests still fail as intended: their server presents a certificate
+ // with no SAN and CN=Broker, which matches neither "localhost" nor the default advertised address.
proxyConfig.setAdvertisedAddress("localhost");
proxyConfig.setClusterName(CLUSTER_NAME);
diff --git a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyWithoutServiceDiscoveryTest.java b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyWithoutServiceDiscoveryTest.java
index 8995b100e3832..07a65fcce6e2f 100644
--- a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyWithoutServiceDiscoveryTest.java
+++ b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/ProxyWithoutServiceDiscoveryTest.java
@@ -107,7 +107,7 @@ protected void setup() throws Exception {
proxyConfig.setWebServicePortTls(Optional.of(0));
// Advertise over loopback so the client->proxy service URL host (localhost) matches the proxy server
// certificate's SubjectAltName (proxy.cert.pem carries DNS:localhost, IP:127.0.0.1). TLS hostname
- // verification is on by default (PIP-478, SAN-only), and the default advertised address resolves to
+ // verification is on by default (PIP-478), and the default advertised address resolves to
// the machine's canonical hostname/IP, which is not in the cert SAN. This keeps verification enabled.
proxyConfig.setAdvertisedAddress("localhost");
proxyConfig.setTlsEnabledWithBroker(true);
diff --git a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/SslContextFallbackSynthesisTest.java b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/SslContextFallbackSynthesisTest.java
index c7434bd41d821..21423d61cf92b 100644
--- a/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/SslContextFallbackSynthesisTest.java
+++ b/pulsar-proxy/src/test/java/org/apache/pulsar/proxy/server/SslContextFallbackSynthesisTest.java
@@ -95,7 +95,7 @@ protected void setup() throws Exception {
proxyConfig.setBrokerProxyAllowedTargetPorts("*");
// Advertise over loopback so the client->proxy service URL host (localhost) matches the proxy server
// certificate's SubjectAltName (proxy.cert.pem carries DNS:localhost, IP:127.0.0.1). The client
- // verifies hostnames (on by default, PIP-478 SAN-only) and the default advertised address resolves
+ // verifies hostnames (on by default, PIP-478) and the default advertised address resolves
// to the machine's canonical hostname/IP, which is not in the cert SAN.
proxyConfig.setAdvertisedAddress("localhost");
proxyConfig.setTlsAllowInsecureConnection(true);
diff --git a/tests/certificate-authority/ec/jks/key_store_generation.txt b/tests/certificate-authority/ec/jks/key_store_generation.txt
index 378b5af898c96..3b270a97e882c 100644
--- a/tests/certificate-authority/ec/jks/key_store_generation.txt
+++ b/tests/certificate-authority/ec/jks/key_store_generation.txt
@@ -7,8 +7,10 @@ keytool -keystore ca.truststore.jks -alias ca -importcert -file ca.cert.pem -sto
# Create server keystore
-# NOTE (Pulsar 5.0 / PIP-478): TLS hostname verification is enabled by default and is SAN-only, so the server
+# NOTE (Pulsar 5.0 / PIP-478): TLS hostname verification is enabled by default, so the server
# certificate MUST carry a SubjectAltName that matches the connected host (e.g. DNS:localhost / IP:127.0.0.1).
+# The CN fallback the default engines apply to a SAN-less certificate does not rescue this fixture: its CN is
+# "server", which matches none of the hosts below, and an IP literal is never matched against the CN anyway.
# The keystore hierarchy has its own CA (this directory's ca.cert.pem / ca.key.pem, trusted via
# ca.truststore.jks) which is SEPARATE from the PEM hierarchy's CA in the parent directory, so the server
# certificate here must be signed by THIS directory's ca.key.pem. `keytool -certreq` drops extensions and
diff --git a/tests/certificate-authority/generate_keystore.sh b/tests/certificate-authority/generate_keystore.sh
index 62836a5b186ea..67900bbf77405 100755
--- a/tests/certificate-authority/generate_keystore.sh
+++ b/tests/certificate-authority/generate_keystore.sh
@@ -27,10 +27,12 @@ DAYS=36500
COMMON_PARAMS="-storetype JKS -storepass 111111 -keypass 111111 -noprompt"
# generate keystore
-# The broker keystore is presented as a TLS *server* certificate. With TLS hostname verification
-# enabled by default in 5.0 (PIP-478) and CN-based matching removed (SAN-only), the server cert must
-# carry a SubjectAltName covering the connected host, otherwise every peer that now verifies hostnames
-# rejects it with "No subject alternative names present". Tests reach the broker/proxy over loopback
+# The broker keystore is presented as a TLS *server* certificate, and TLS hostname verification is
+# enabled by default in 5.0 (PIP-478), so it carries a SubjectAltName covering the connected host.
+# CN=localhost on its own would satisfy the JDK and OpenSSL engines, which fall back to the CN for a
+# certificate with no dNSName SAN — but not a Conscrypt-pinned peer, which does not fall back, and not a
+# connection made to the IP literal, which is never matched against the CN. The SAN is what makes this
+# fixture portable across providers and endpoint forms. Tests reach the broker/proxy over loopback
# (advertised address pinned to localhost), so DNS:localhost + IP:127.0.0.1 is sufficient. The client
# and proxy keystores below are only ever used as *client* identities (not hostname-verified), so they
# intentionally keep no SAN.