diff --git a/conf/client.conf b/conf/client.conf index df44375cd2bd4..493e5a734839d 100644 --- a/conf/client.conf +++ b/conf/client.conf @@ -46,8 +46,9 @@ authParams= # authority. tlsAllowInsecureConnection=false -# Whether the server hostname is verified against the subject alternative names (SANs) of the -# certificate the server is using (RFC 2818/6125; CN matching is not supported). +# Whether the server hostname is verified against the certificate the server is using. The subject +# alternative names (SANs) are authoritative; the CN is consulted only when the certificate carries no +# dNSName SAN (RFC 2818 section 3.1). A Conscrypt-pinned client does not fall back to the CN at all. # Enabled by default since Pulsar 5.0 (PIP-478). tlsEnableHostnameVerification=true diff --git a/conf/functions_worker.yml b/conf/functions_worker.yml index e6dfbc35255af..6d92c939f744d 100644 --- a/conf/functions_worker.yml +++ b/conf/functions_worker.yml @@ -348,8 +348,9 @@ tlsKeyFilePath: tlsTrustCertsFilePath: # Accept untrusted TLS certificate from client tlsAllowInsecureConnection: false -# Whether the server hostname is verified against the subject alternative names (SANs) of the -# server certificate (RFC 2818/6125; CN matching is not supported). +# Whether the server hostname is verified against the server certificate. The subject alternative names +# (SANs) are authoritative; the CN is consulted only when the certificate carries no dNSName SAN +# (RFC 2818 section 3.1). A Conscrypt-pinned client does not fall back to the CN at all. # Enabled by default since Pulsar 5.0 (PIP-478). tlsEnableHostnameVerification: true # Tls cert refresh duration in seconds. Set 0 to disable the background rotation check, so the diff --git a/pip/pip-478.md b/pip/pip-478.md index 9419f67750f10..bacce78e4c15c 100644 --- a/pip/pip-478.md +++ b/pip/pip-478.md @@ -1,6 +1,6 @@ # PIP-478: Asynchronous v5 client authentication SPI, Pulsar TLS factory SPI, and Pulsar HTTP client SPI -> **This PIP at a glance.** It introduces three SPIs — an async, capability-segregated **v5 authentication SPI** (replacing PIP-466's synchronous `Authentication` stub), a purpose-driven **`PulsarTlsFactory` SPI**, and a framework-managed **`PulsarHttpClient` SPI** — and, as a deliberate **breaking change**, **removes PIP-337's `PulsarSslFactory` entirely**. It also makes Pulsar 5.0 TLS **secure by default** (hostname verification on, SAN-only). Reviewer's map: [Motivation](#motivation) (five concrete v4 problems) → [High Level Design](#high-level-design) (the shapes and the reasoning) → [Detailed Design](#detailed-design) (full type listings) → [PIP-337 removal impact](#pip-337-removal-impact) (disposition of every removed config key / method) → [Resolved design decisions](#resolved-design-decisions) (options weighed). Security- and compatibility-focused reviewers should also read [Security Considerations](#security-considerations) and [Backward & Forward Compatibility](#backward--forward-compatibility). +> **This PIP at a glance.** It introduces three SPIs — an async, capability-segregated **v5 authentication SPI** (replacing PIP-466's synchronous `Authentication` stub), a purpose-driven **`PulsarTlsFactory` SPI**, and a framework-managed **`PulsarHttpClient` SPI** — and, as a deliberate **breaking change**, **removes PIP-337's `PulsarSslFactory` entirely**. It also makes Pulsar 5.0 TLS **secure by default** (hostname verification on). Reviewer's map: [Motivation](#motivation) (five concrete v4 problems) → [High Level Design](#high-level-design) (the shapes and the reasoning) → [Detailed Design](#detailed-design) (full type listings) → [PIP-337 removal impact](#pip-337-removal-impact) (disposition of every removed config key / method) → [Resolved design decisions](#resolved-design-decisions) (options weighed). Security- and compatibility-focused reviewers should also read [Security Considerations](#security-considerations) and [Backward & Forward Compatibility](#backward--forward-compatibility). # Background knowledge @@ -1242,7 +1242,11 @@ New OpenTelemetry instruments: # Security Considerations - **TLS is secure by default in 5.0.** Hostname verification is **ON by default** for both the v4 and v5 client *and* for every broker / proxy / geo-replication **outbound** (broker-as-client) connection — a peer whose certificate does not match its hostname/SAN is rejected. Concretely, PIP-478 flips these defaults from `false` to `true`: `tlsHostnameVerificationEnable` on the v4 `ClientConfigurationData` (which also flows into the `CLIENT_DEFAULT` `TlsPolicy`), `tlsHostnameVerificationEnabled` on the broker `ServiceConfiguration` (geo-replication / broker-to-broker lookup) and the proxy `ProxyConfiguration` (proxy→broker), the websocket `WebSocketProxyConfiguration`, and `tlsEnableHostnameVerification` on the functions-worker `WorkerConfig` (their broker-facing clients). The v5 `TlsPolicy` already defaulted secure (`enableHostnameVerification()` true). `allowInsecureConnection` stays **false** everywhere — untrusted server certificates are rejected — unchanged from 4.x. (The dev-only `TlsPolicy.insecure()` preset and the separate BookKeeper-client passthrough `bookkeeper_tlsHostnameVerificationEnabled` are intentionally left as-is.) Enabling any insecure setting is logged once at WARN level, on first use. -- **SAN-only hostname matching (RFC 6125).** Deprecated CN-field hostname matching is **removed**: the custom `TlsHostnameVerifier` and its CN machinery (`SubjectName`, `PublicSuffixMatcher` / `PublicSuffixList`, `DomainType`) are deleted, and both the JDK path (endpoint-identification algorithm `"HTTPS"`) and the Conscrypt / OpenSSL path now perform standard SAN-based (RFC 2818) verification only. A server certificate must carry the hostname in its SubjectAltName extension; a certificate that matches only via its CN field is no longer accepted. This affects **server-hostname** matching only — a *client* certificate's CN is still read as the role token by the broker's `AuthenticationProviderTls`, unchanged. +- **Pulsar's own CN-matching machinery is removed.** The custom `TlsHostnameVerifier` and its CN machinery (`SubjectName`, `PublicSuffixMatcher` / `PublicSuffixList`, `DomainType`) are deleted; hostname verification is whatever the configured provider implements under the standard endpoint-identification algorithm `"HTTPS"`. + + Be precise about what that does and does not change. The standard algorithm is **not** SAN-only: per RFC 2818 §3.1, and as the JDK's `HostnameChecker` implements it, the CN is consulted **when the client connects by hostname and the certificate carries no `dNSName` SAN**, and ignored once any is present. (RFC 6125 §6.4.4's condition is stricter than that — it permits the CN check only when the certificate presents *no* DNS-ID, SRV-ID or URI-ID at all — so the behaviour Pulsar actually gets is RFC 2818's, not RFC 6125's.) The fallback is for hostnames only — a client connecting to an IP literal is matched against `iPAddress` SANs and never against the CN, so a SAN-less certificate has never worked for an IP connection, before or after this change. That holds on both default engines — Netty's native (OpenSSL/BoringSSL) engine does not verify the hostname itself, it delegates to the Java trust manager, so the JDK's `HostnameChecker` and its CN fallback apply there too. So on the default engines a CN-only certificate is still accepted, before and after this change. What the removal does change is **Conscrypt-pinned clients**: `SecurityUtility`'s static initializer used to install the CN-tolerant `TlsHostnameVerifier` as Conscrypt's process-global default. With it gone, Conscrypt applies its own SAN-only verification and rejects a server certificate with no `subjectAltName`. Note that this is *stricter* than RFC 2818, not an application of it: RFC 2818 §3.1 mandates the CN fallback ("Otherwise, the (most specific) Common Name field ... MUST be used"), and RFC 6125 §6.4.4 permitted it as a last resort. Conscrypt implements neither — and that is not a Conscrypt eccentricity but the *current* standard: RFC 9525 (2023) obsoletes RFC 6125 and forbids the CN check outright ("The Common Name RDN MUST NOT be used to identify a service"). So the default engines are the lenient ones here, and what Pulsar's CN-only certificates have always relied on is behaviour the IETF has since retired. A deployment is affected only if it explicitly names Conscrypt as its *client-side* JSSE provider and still uses CN-only server certificates; the remedy is to reissue those certificates with the hostname in the SAN. Conscrypt is never the client-side default — it is defaulted only for `TlsPurpose.WEB`, i.e. server listeners, which do not verify hostnames. + + This affects **server-hostname** matching only — a *client* certificate's CN is still read as the role token by the broker's `AuthenticationProviderTls`, unchanged. - **A configured truststore never silently widens trust.** On the **keystore** axis a configured `trustStorePath` whose store holds zero X.509 certificates **fails the load** instead of resolving to an empty trust list, which the context builders would treat as "not configured" and satisfy with the *platform default* trust store — silently trusting every public CA. This restores the 4.x `KeyStoreSSLContext` behavior (the `TrustManagerFactory` was initialized with the explicit store, so a zero-entry store rejected every peer), and because the rotation baseline is committed only after a successful load, an accidental rotation to an empty store keeps the last-good material, WARNs, and retries. The **PEM** axis deliberately keeps the 4.x behavior (an empty/zero-certificate `trustCertsFilePath` falls back to the platform trust store, as `SecurityUtility.setupTrustCerts` did) so existing deployments are not broken — but it now WARNs naming the file. - **A half-configured identity fails loudly — on both axes.** On the **PEM** axis a `certificateFilePath` without a `keyFilePath` yields no usable identity and was silently dropped, surfacing only as a handshake/authentication failure; the default factory now rejects it at load, naming both fields. The converse is asymmetric on purpose: a `keyFilePath` without a certificate is what 4.x tolerated, so it stays a WARN rather than a new startup failure. On the **keystore** axis the counterpart is a *configured* `keyStorePath` whose store yields no usable key entry (a private key with an X.509 chain) — that fails the load too. The 4.x bound is worth stating precisely, because the two sub-cases differ: a wrong `keyStorePassword` already threw `UnrecoverableKeyException` in 4.x and still does (parity, raised before this check), whereas a store holding *no* key entry initialized 4.x's `KeyManagerFactory` fine but with no aliases — a certain, undiagnosed handshake failure for a server purpose, and a silently identity-less client against a peer that does not request a certificate. Failing the load is therefore a deliberate tightening whose only casualties are deployments that already presented no identity, and setting `keyStorePath` is an unambiguous statement of intent to present one. Keep-last-good applies on both axes: the rotation baseline is committed only after a successful load, so an accidental rotation to a keyless store retains the last-good material, WARNs, and retries. All these checks live in the default factory's material load, not in `TlsPolicy.Builder`, so a custom `PulsarTlsFactory` building its own policies is not constrained by them. - **Default enabled protocol set.** When no protocols are configured, the framework pins **`{TLSv1.3, TLSv1.2}`** on the built contexts — the native Netty, the JDK-synthesis, and the Jetty include-protocols paths alike — preserving the floor the removed `DefaultPulsarSslFactory` forced rather than silently deferring to the JVM/provider default on upgrade. @@ -1253,7 +1257,7 @@ New OpenTelemetry instruments: # Backward & Forward Compatibility -- **BREAKING CHANGE — TLS hostname verification on by default + SAN required (5.0).** A v4 or v5 client — and any broker / proxy / geo-replication **outbound** connection — that connects to a server whose certificate lacks a SubjectAltName matching the target hostname now **fails the TLS handshake**, where 4.x (hostname verification off by default) silently accepted it. Deprecated CN-field matching is also removed, so a CN-only certificate no longer suffices even when its CN names the host. This is a deliberate secure-by-default hardening — it closes the long-standing "hostname verification off by default is MITM-prone" gap. **Remediation:** regenerate the affected server certificates so each carries a SAN matching the hostname clients connect to (see the pulsar-site *Transport Encryption (TLS)* guide, `security-tls-transport.md`); or, only where a certificate cannot yet be reissued and the risk is understood, restore the prior per-connection behavior by setting `tlsHostnameVerificationEnable=false` (v4/v5 client), `tlsHostnameVerificationEnabled=false` (broker / proxy / websocket), or `tlsEnableHostnameVerification=false` (functions-worker) — **not recommended**, as it re-opens the connection to man-in-the-middle attacks. `allowInsecureConnection` is independent of this and still defaults to `false` (untrusted server certificates are rejected). +- **BREAKING CHANGE — TLS hostname verification on by default (5.0).** A v4 or v5 client — and any broker / proxy / geo-replication **outbound** connection — that connects to a server whose certificate does not match the target hostname now **fails the TLS handshake**, where 4.x (hostname verification off by default) silently accepted it. Matching follows the provider's standard endpoint-identification algorithm: a SubjectAltName matching the hostname always suffices, and on the default engines the CN is still consulted for a certificate that carries no `dNSName` SAN. Pulsar's own CN-matching verifier is removed, which tightens verification for **Conscrypt-pinned clients** specifically — Conscrypt does not fall back to the CN, and the relaxed global verifier that used to make it do so is gone. This is a deliberate secure-by-default hardening — it closes the long-standing "hostname verification off by default is MITM-prone" gap. **Remediation:** regenerate the affected server certificates so each carries a SAN matching the hostname clients connect to — which is also what a Conscrypt-pinned client requires (see the pulsar-site *Transport Encryption (TLS)* guide, `security-tls-transport.md`); or, only where a certificate cannot yet be reissued and the risk is understood, restore the prior per-connection behavior by setting `tlsHostnameVerificationEnable=false` (v4/v5 client), `tlsHostnameVerificationEnabled=false` (broker / proxy / websocket), or `tlsEnableHostnameVerification=false` (functions-worker) — **not recommended**, as it re-opens the connection to man-in-the-middle attacks. `allowInsecureConnection` is independent of this and still defaults to `false` (untrusted server certificates are rejected). - The v5 client is published as a standalone artifact in 5.0.0-M1 (the existing `pulsar-client-v5` and `pulsar-client-api-v5` modules from PIP-466), **unshaded**. The v5 client will not ship a shaded distribution; applications that need shading to resolve a dependency conflict relocate it themselves in their own build, as they would any other dependency (see the plugin-packaging note above). - **BREAKING CHANGE — PIP-337 removed (5.0).** PIP-337 is removed from Pulsar in Pulsar 5.0 since retaining it in the code base causes an additional maintenance burden and most users don't use it. Concretely: the `sslFactoryPlugin` / `sslFactoryPluginParams` / `brokerClientSslFactoryPlugin` / `brokerClientSslFactoryPluginParams` **config-file keys are removed** and a stale non-default value is **rejected at broker/proxy startup** (and at client `loadConf`) by a removed-key validation; the v4 `ClientBuilder` / `PulsarAdminBuilder` `sslFactoryPlugin(...)` / `sslFactoryPluginParams(...)` methods are **removed** (a source-compatibility break — a compile error on upgrade); the `ClusterData.brokerClientSslFactoryPlugin` / `...Params` fields are **deprecated and ignored** rather than removed — they stay in the metadata schema so a cluster written by 4.x still round-trips — and their successors `ClusterData.brokerClientTlsFactoryClassName` / `brokerClientTlsFactoryConfig` are **added**; and the `pulsar-admin clusters` `--tls-factory-plugin` / `--tls-factory-plugin-params` CLI options (`CmdClusters`) are **replaced** by `--tls-factory-class-name` / `--tls-factory-config`, which write the successor fields, so per-cluster factory selection is retained. Existing PIP-337 users migrate to the new `PulsarTlsFactory` SPI, selected by `tlsFactoryClassName` / `tlsFactoryConfig` — on server config **and**, new in this PIP, on the v4 client and admin builders — the affected public keys/methods/CLI options and each one's disposition are inventoried in [PIP-337 removal impact](#pip-337-removal-impact), and a migration sketch is under Upgrade below. - **Security-relevant behavior changes (5.0), surfaced here so they aren't buried in the removal inventory:** @@ -1276,7 +1280,7 @@ New OpenTelemetry instruments: ## Upgrade - Existing applications using `pulsar-client-api` (v4) need **no source changes** — except the rare application that called the removed PIP-337 `sslFactoryPlugin(...)` / `sslFactoryPluginParams(...)` builder methods, which no longer compile and must move to `tlsFactoryClassName(...)` / `tlsFactoryConfig(...)` (see below) — but 5.0 carries several **runtime behavior deltas** they should review before upgrading: - - **Hostname verification is on by default** (`tlsHostnameVerificationEnable` now defaults `true`) and **CN-based matching is removed** (a matching SAN is required) — a server certificate without a matching SAN now fails the TLS handshake where 4.x silently accepted it. See [Security Considerations](#security-considerations) and the breaking-change note under [Backward & Forward Compatibility](#backward--forward-compatibility). + - **Hostname verification is on by default** (`tlsHostnameVerificationEnable` now defaults `true`) — a server certificate that does not match the host now fails the handshake where 4.x silently accepted it. Pulsar's own CN-matching verifier is also removed, which tightens verification for **Conscrypt-pinned clients** specifically; on the default engines the standard algorithm still falls back to the CN for a certificate with no SAN. See [Security Considerations](#security-considerations) and the breaking-change note under [Backward & Forward Compatibility](#backward--forward-compatibility). - **TLS material loads eagerly at client build** (the fail-fast contract), not lazily at first connection as in v4 — a misconfigured cert/key/trust path now surfaces at `PulsarClient` build instead of on first connect. - **`sslProvider` provider-name now routes to `jsseProvider`**: the `OPENSSL` / `OPENSSL_REFCNT` literals still select the native Netty engine; a JSSE *provider name* (e.g. `Conscrypt`, `SunJSSE`) that the old code accepted is now carried to `TlsPolicy.jsseProvider` and builds the `SSLContext` on the JDK engine (**v4 parity restored**), failing loudly if the name resolves to no installed provider (see [v4 client `tls*` settings](#pip-337-removal-impact)). - **The new `jcaProvider` key is a no-op on upgrade.** `jcaProvider` (and `brokerClientJcaProvider`) is unset by default on every surface, and unset means every material `getInstance` call keeps its one-argument form — the JVM provider search order, i.e. the behaviour of every release before this PIP. No config file needs editing, and unlike `jsseProvider` **no legacy v4 value is ever routed onto this axis**, so nothing can start landing there by surprise. Only deployments that opt in see a change; for them the store-type interaction under [Configuration](#configuration) applies. diff --git a/pulsar-broker-common/build.gradle.kts b/pulsar-broker-common/build.gradle.kts index b9d2b00d1dd9a..5c2d700607fe5 100644 --- a/pulsar-broker-common/build.gradle.kts +++ b/pulsar-broker-common/build.gradle.kts @@ -46,8 +46,8 @@ dependencies { implementation(libs.jetty.compression.gzip) implementation(libs.jetty.ee10.servlet) - // Non-FIPS BouncyCastle provider for tests that exercise SecurityUtility (which loads - // org.bouncycastle.jce.provider.BouncyCastleProvider in a static initializer). This matches + // Non-FIPS BouncyCastle provider for tests that exercise JcaProviders (which resolves + // org.bouncycastle.jce.provider.BouncyCastleProvider reflectively, on first use). This matches // the provider used in production. FIPS is covered separately by the bcfips-include-test // module; bc-fips must not be on a classpath that also has the non-FIPS provider because both // jars define org.bouncycastle.* and the JVM rejects the mismatched signers. diff --git a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/tls/TlsFactorySupport.java b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/tls/TlsFactorySupport.java index c63e78f819356..4d85b4505e1ff 100644 --- a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/tls/TlsFactorySupport.java +++ b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/tls/TlsFactorySupport.java @@ -293,7 +293,7 @@ public static SslProvider engineProvider(String providerString) { * is conditional, because a default that breaks a supported platform is not a usable default. * *

This applies to server listeners only. Client-side hostname verification is unaffected: a server - * does not verify hostnames, so pinning Conscrypt here cannot interact with the SAN-only verification + * does not verify hostnames, so pinning Conscrypt here cannot interact with the hostname verification * PIP-478 turns on. * * @param explicitJsseProvider the configured {@code jsseProvider} (may be null/blank) diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/client/api/AuthenticationTlsHostnameVerificationTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/client/api/AuthenticationTlsHostnameVerificationTest.java index 82db1c79c17e9..0dca46fe3133e 100644 --- a/pulsar-broker/src/test/java/org/apache/pulsar/client/api/AuthenticationTlsHostnameVerificationTest.java +++ b/pulsar-broker/src/test/java/org/apache/pulsar/client/api/AuthenticationTlsHostnameVerificationTest.java @@ -125,8 +125,9 @@ public Object[][] codecProvider() { * 1. Client tries to connect to broker with hostname="localhost" * 2. Broker sends an x509 certificate whose SubjectAltName does not match "localhost" * (in fact it has no SAN at all, only CN=broker.pulsar.apache.org) - * 3. Client performs SAN-based (RFC 2818) host-name verification, closes the connection - * and fails consumer creation + * 3. Client performs host-name verification: there is no SAN to match, and the CN the default + * engines fall back to is "broker.pulsar.apache.org", so it closes the connection and fails + * consumer creation * * * @throws Exception @@ -140,7 +141,7 @@ public void testTlsSyncProducerAndConsumerWithInvalidBrokerHost(boolean hostname this.hostnameVerificationEnabled = hostnameVerificationEnabled; clientTrustCertFilePath = TLS_MIM_TRUST_CERT_FILE_PATH; // setup broker cert which has no SAN matching the broker's hostname="localhost" - // (CN=broker.pulsar.apache.org, no subjectAltName): CN-based matching is no longer supported (PIP-478). + // (CN=broker.pulsar.apache.org, no subjectAltName), so it matches neither by SAN nor by CN fallback. // The broker itself uses this deliberately-mismatched cert, so its own internal client (e.g. the // system-topic reader for topic policies) cannot verify "localhost" against it; disable hostname // verification for the broker's outbound client here. This test exercises the end-user client's @@ -179,7 +180,7 @@ public void testTlsSyncProducerAndConsumerWithInvalidBrokerHost(boolean hostname *

      * 1. Client tries to connect to broker with hostname="localhost"
      * 2. Broker sends an x509 certificate whose SubjectAltName includes DNS:localhost
-     * 3. Client performs SAN-based (RFC 2818) host-name verification and continues
+     * 3. Client performs host-name verification, matches DNS:localhost in the SAN, and continues
      * 
* * @throws Exception diff --git a/pulsar-client-admin-api/src/main/java/org/apache/pulsar/client/admin/PulsarAdminBuilder.java b/pulsar-client-admin-api/src/main/java/org/apache/pulsar/client/admin/PulsarAdminBuilder.java index a9fb95a121110..8fd959238ebb9 100644 --- a/pulsar-client-admin-api/src/main/java/org/apache/pulsar/client/admin/PulsarAdminBuilder.java +++ b/pulsar-client-admin-api/src/main/java/org/apache/pulsar/client/admin/PulsarAdminBuilder.java @@ -205,6 +205,11 @@ PulsarAdminBuilder authentication(String authPluginClassName, MapThe 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 rfc2818 * * @param enableTlsHostnameVerification diff --git a/pulsar-client-admin/src/test/java/org/apache/pulsar/client/admin/internal/AdminOAuth2IdpTlsEndToEndTest.java b/pulsar-client-admin/src/test/java/org/apache/pulsar/client/admin/internal/AdminOAuth2IdpTlsEndToEndTest.java index 40d7b3e572da9..08473bdc5de58 100644 --- a/pulsar-client-admin/src/test/java/org/apache/pulsar/client/admin/internal/AdminOAuth2IdpTlsEndToEndTest.java +++ b/pulsar-client-admin/src/test/java/org/apache/pulsar/client/admin/internal/AdminOAuth2IdpTlsEndToEndTest.java @@ -69,7 +69,8 @@ public class AdminOAuth2IdpTlsEndToEndTest { // broker.cert.pem carries SAN:localhost and chains to ca.cert.pem, so both trust and hostname verification - // pass against the WireMock server (the shared broker.keystore.jks has no SAN). + // pass against the WireMock server (the shared broker.keystore.jks carries the same SAN but is self-signed, + // so it fails trust verification rather than hostname verification). private static final String BROKER_CERT = resource("certificate-authority/server-keys/broker.cert.pem"); private static final String BROKER_KEY = resource("certificate-authority/server-keys/broker.key-pk8.pem"); private static final String CA_CERT = resource("certificate-authority/certs/ca.cert.pem"); diff --git a/pulsar-client-api/src/main/java/org/apache/pulsar/client/api/ClientBuilder.java b/pulsar-client-api/src/main/java/org/apache/pulsar/client/api/ClientBuilder.java index 2966ca993bc49..7ef5ca1c1dd84 100644 --- a/pulsar-client-api/src/main/java/org/apache/pulsar/client/api/ClientBuilder.java +++ b/pulsar-client-api/src/main/java/org/apache/pulsar/client/api/ClientBuilder.java @@ -381,7 +381,12 @@ ClientBuilder authentication(String authPluginClassName, Map 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.