diff --git a/vertx-pg-client/pom.xml b/vertx-pg-client/pom.xml
index f571805d8..bab6a9bfb 100644
--- a/vertx-pg-client/pom.xml
+++ b/vertx-pg-client/pom.xml
@@ -51,7 +51,7 @@
com.ongres.scram
scram-client
- 3.2
+ 3.4
diff --git a/vertx-pg-client/src/main/asciidoc/index.adoc b/vertx-pg-client/src/main/asciidoc/index.adoc
index 2d7a6562c..3c42f9563 100644
--- a/vertx-pg-client/src/main/asciidoc/index.adoc
+++ b/vertx-pg-client/src/main/asciidoc/index.adoc
@@ -195,6 +195,7 @@ Currently, the client supports the following parameter keys:
* `dbname`
* `sslmode`
* `sslnegotiation`
+* `channel_binding`
Additional parameters will be added to the {@link io.vertx.sqlclient.SqlConnectOptions#getProperties properties}.
@@ -214,6 +215,7 @@ for more details. The following parameters are supported:
* `PGPASSWORD`
* `PGSSLMODE`
* `PGSSLNEGOTIATION`
+* `PGCHANNELBINDING`
If you don't specify a data object or a connection URI string to connect, environment variables will take precedence over them.
@@ -224,7 +226,8 @@ $ PGUSER=user \
PGPASSWORD=secret \
PGDATABASE=the-db \
PGPORT=5432 \
- PGSSLMODE=DISABLE
+ PGSSLMODE=DISABLE \
+ PGCHANNELBINDING=prefer
----
[source,$lang]
@@ -767,6 +770,36 @@ Attempting to use `DIRECT` mode with older PostgreSQL versions will result in an
More information can be found in the http://vertx.io/docs/vertx-core/java/#ssl[Vert.x documentation].
+=== Channel Binding (SCRAM authentication)
+
+Channel binding is a method for the server to authenticate itself to the client.
+It is only supported over SSL connections with PostgreSQL 11 or later servers using the `SCRAM authentication` method.
+
+The client supports three channel binding modes via {@link io.vertx.pgclient.ChannelBinding}:
+
+- `DISABLE`: Prevents the use of channel binding
+- `PREFER` (default): Means that the client will choose channel binding if available
+- `REQUIRE`: Means that the connection must employ channel binding
+
+[source,$lang]
+----
+{@link examples.PgClientExamples#channelBinding}
+----
+
+You can also configure Channel binding via connection URI:
+
+[source,$lang]
+----
+{@link examples.PgClientExamples#channelBindingUri}
+----
+
+Or using the environment variable:
+
+[source,bash]
+----
+export PGCHANNELBINDING=require
+----
+
== Using a level 4 proxy
You can configure the client to use an HTTP/1.x CONNECT, SOCKS4a or SOCKS5 level 4 proxy.
diff --git a/vertx-pg-client/src/main/java/examples/PgClientExamples.java b/vertx-pg-client/src/main/java/examples/PgClientExamples.java
index 032d7bcf8..9e3069174 100644
--- a/vertx-pg-client/src/main/java/examples/PgClientExamples.java
+++ b/vertx-pg-client/src/main/java/examples/PgClientExamples.java
@@ -539,6 +539,33 @@ public void sslNegotiationUri() {
PgConnectOptions options = PgConnectOptions.fromUri(connectionUri);
}
+ public void channelBinding(Vertx vertx) {
+ PgConnectOptions options = new PgConnectOptions()
+ .setPort(5432)
+ .setHost("the-host")
+ .setDatabase("the-db")
+ .setUser("user")
+ .setPassword("secret")
+ .setSslMode(SslMode.REQUIRE)
+ .setChannelBinding(ChannelBinding.REQUIRE)
+ .setSslOptions(new ClientSSLOptions()
+ .setTrustOptions(new PemTrustOptions().addCertPath("/path/to/server.crt")));
+
+ PgConnection.connect(vertx, options)
+ .onComplete(res -> {
+ if (res.succeeded()) {
+ System.out.println("Connected with channel binding enforcement");
+ } else {
+ System.out.println("Could not connect: " + res.cause().getMessage());
+ }
+ });
+ }
+
+ public void channelBindingUri() {
+ String connectionUri = "postgresql://localhost/mydb?sslmode=require&channel_binding=require";
+ PgConnectOptions options = PgConnectOptions.fromUri(connectionUri);
+ }
+
public void jsonExample() {
// Create a tuple
diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/ChannelBinding.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/ChannelBinding.java
new file mode 100644
index 000000000..6ee9ec511
--- /dev/null
+++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/ChannelBinding.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ */
+package io.vertx.pgclient;
+
+import io.vertx.codegen.annotations.VertxGen;
+
+/**
+ * The different values for the Channel Binding parameter provide different levels of
+ * protection. Channel binding is a method for the server to authenticate itself to the client.
+ * It is only supported over SSL connections with PostgreSQL 11 or later servers using the
+ * SCRAM authentication method.
+ *
+ * @see
+ * libpq channel_binding
+ */
+@VertxGen
+public enum ChannelBinding {
+
+ /**
+ * Prevents the use of channel binding
+ */
+ DISABLE("disable"),
+
+ /**
+ * Means that the client will choose channel binding if available.
+ */
+ PREFER("prefer"),
+
+ /**
+ * Means that the connection must employ channel binding.
+ */
+ REQUIRE("require");
+
+ public static final ChannelBinding[] VALUES = ChannelBinding.values();
+
+ public final String value;
+
+ ChannelBinding(String value) {
+ this.value = value;
+ }
+
+ public static ChannelBinding of(String value) {
+ for (ChannelBinding channelBinding : VALUES) {
+ if (channelBinding.value.equalsIgnoreCase(value)) {
+ return channelBinding;
+ }
+ }
+
+ throw new IllegalArgumentException("Could not find an appropriate Channel Binding mode for the value [" + value + "].");
+ }
+}
diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/PgConnectOptions.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/PgConnectOptions.java
index 62e63216b..520535a8b 100644
--- a/vertx-pg-client/src/main/java/io/vertx/pgclient/PgConnectOptions.java
+++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/PgConnectOptions.java
@@ -99,6 +99,9 @@ public static PgConnectOptions fromEnv() {
if (getenv("PGSSLNEGOTIATION") != null) {
pgConnectOptions.setSslNegotiation(SslNegotiation.of(getenv("PGSSLNEGOTIATION")));
}
+ if (getenv("PGCHANNELBINDING") != null) {
+ pgConnectOptions.setChannelBinding(ChannelBinding.of(getenv("PGCHANNELBINDING")));
+ }
return pgConnectOptions;
}
@@ -110,6 +113,7 @@ public static PgConnectOptions fromEnv() {
public static final int DEFAULT_PIPELINING_LIMIT = 256;
public static final SslMode DEFAULT_SSLMODE = SslMode.DISABLE;
public static final SslNegotiation DEFAULT_SSL_NEGOTIATION = SslNegotiation.POSTGRES;
+ public static final ChannelBinding DEFAULT_CHANNEL_BINDING = ChannelBinding.PREFER;
public static final boolean DEFAULT_USE_LAYER_7_PROXY = false;
public static final Map DEFAULT_PROPERTIES;
@@ -125,6 +129,7 @@ public static PgConnectOptions fromEnv() {
private int pipeliningLimit = DEFAULT_PIPELINING_LIMIT;
private SslMode sslMode = DEFAULT_SSLMODE;
private SslNegotiation sslNegotiation = DEFAULT_SSL_NEGOTIATION;
+ private ChannelBinding channelBinding = DEFAULT_CHANNEL_BINDING;
private boolean useLayer7Proxy = DEFAULT_USE_LAYER_7_PROXY;
public PgConnectOptions() {
@@ -143,6 +148,7 @@ public PgConnectOptions(SqlConnectOptions other) {
pipeliningLimit = opts.pipeliningLimit;
sslMode = opts.sslMode;
sslNegotiation = opts.sslNegotiation;
+ channelBinding = opts.channelBinding;
}
}
@@ -151,6 +157,7 @@ public PgConnectOptions(PgConnectOptions other) {
pipeliningLimit = other.pipeliningLimit;
sslMode = other.sslMode;
sslNegotiation = other.sslNegotiation;
+ channelBinding = other.channelBinding;
}
@Override
@@ -258,6 +265,24 @@ public PgConnectOptions setSslNegotiation(SslNegotiation sslNegotiation) {
return this;
}
+ /**
+ * @return the value of current Channel Binding mode
+ */
+ public ChannelBinding getChannelBinding() {
+ return channelBinding;
+ }
+
+ /**
+ * Set {@link ChannelBinding} for the client, this option controls the client's use of channel binding.
+ *
+ * @param channelBinding the channel binding mode
+ * @return a reference to this, so the API can be used fluently
+ */
+ public PgConnectOptions setChannelBinding(ChannelBinding channelBinding) {
+ this.channelBinding = channelBinding;
+ return this;
+ }
+
/**
* @return whether the client interacts with a layer 7 proxy instead of a server
*/
@@ -342,6 +367,7 @@ public boolean equals(Object o) {
if (pipeliningLimit != that.pipeliningLimit) return false;
if (sslMode != that.sslMode) return false;
if (sslNegotiation != that.sslNegotiation) return false;
+ if (channelBinding != that.channelBinding) return false;
return true;
}
@@ -352,6 +378,7 @@ public int hashCode() {
result = 31 * result + pipeliningLimit;
result = 31 * result + sslMode.hashCode();
result = 31 * result + sslNegotiation.hashCode();
+ result = 31 * result + channelBinding.hashCode();
return result;
}
diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgConnectionUriParser.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgConnectionUriParser.java
index 6d8858657..32848693a 100644
--- a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgConnectionUriParser.java
+++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgConnectionUriParser.java
@@ -11,6 +11,7 @@
package io.vertx.pgclient.impl;
import io.vertx.core.json.JsonObject;
+import io.vertx.pgclient.ChannelBinding;
import io.vertx.pgclient.SslMode;
import io.vertx.pgclient.SslNegotiation;
@@ -183,6 +184,9 @@ private static void parseParameters(String parametersInfo, JsonObject configurat
case "sslnegotiation":
configuration.put("sslNegotiation", SslNegotiation.of(value));
break;
+ case "channel_binding":
+ configuration.put("channelBinding", ChannelBinding.of(value));
+ break;
default:
properties.put(key, value);
break;
diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgSocketConnection.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgSocketConnection.java
index 0f332a135..cc046f152 100644
--- a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgSocketConnection.java
+++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgSocketConnection.java
@@ -25,6 +25,7 @@
import io.vertx.core.spi.metrics.ClientMetrics;
import io.vertx.core.internal.ContextInternal;
import io.vertx.core.internal.net.NetSocketInternal;
+import io.vertx.pgclient.ChannelBinding;
import io.vertx.pgclient.PgConnectOptions;
import io.vertx.pgclient.PgException;
import io.vertx.pgclient.impl.codec.ExtendedQueryPgCommandMessage;
@@ -138,6 +139,10 @@ public int getSecretKey() {
return secretKey;
}
+ public ChannelBinding channelBinding() {
+ return connectOptions.getChannelBinding();
+ }
+
@Override
public DatabaseMetadata databaseMetadata() {
return dbMetaData;
diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramAuthentication.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramAuthentication.java
index 658977c4f..b439357f8 100644
--- a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramAuthentication.java
+++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramAuthentication.java
@@ -1,8 +1,8 @@
package io.vertx.pgclient.impl.auth.scram;
-import com.ongres.scram.client.ScramClient;
import io.vertx.core.internal.logging.Logger;
import io.vertx.core.internal.logging.LoggerFactory;
+import io.vertx.pgclient.ChannelBinding;
public class ScramAuthentication {
@@ -13,10 +13,10 @@ public class ScramAuthentication {
static {
ScramAuthentication instance;
try {
- ScramClient.MechanismsBuildStage builder = ScramClient.builder();
- logger.debug("Scram authentication is available " + builder);
+ Class.forName("com.ongres.scram.client.ScramClient");
instance = new ScramAuthentication();
} catch (Throwable notFound) {
+ logger.debug("SCRAM authentication is NOT available");
instance = null;
}
INSTANCE = instance;
@@ -25,7 +25,7 @@ public class ScramAuthentication {
private ScramAuthentication() {
}
- public ScramSession session(String username, char[] password) {
- return new ScramSessionImpl(username, password);
+ public ScramSession session(String username, char[] password, ChannelBinding channelBinding) {
+ return new ScramSessionImpl(username, password, channelBinding);
}
}
diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramSessionImpl.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramSessionImpl.java
index 66fb7e797..fffb26437 100644
--- a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramSessionImpl.java
+++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramSessionImpl.java
@@ -17,43 +17,57 @@
package io.vertx.pgclient.impl.auth.scram;
+import java.nio.charset.StandardCharsets;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSession;
+
+import com.ongres.scram.client.ChannelBindingException;
+import com.ongres.scram.client.ChannelBindingPolicy;
import com.ongres.scram.client.ScramClient;
import com.ongres.scram.common.StringPreparation;
import com.ongres.scram.common.exception.ScramInvalidServerSignatureException;
import com.ongres.scram.common.exception.ScramParseException;
+import com.ongres.scram.common.exception.ScramRuntimeException;
import com.ongres.scram.common.exception.ScramServerErrorException;
import com.ongres.scram.common.util.TlsServerEndpoint;
+
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.ssl.SslHandler;
+import io.vertx.core.internal.logging.Logger;
+import io.vertx.core.internal.logging.LoggerFactory;
+import io.vertx.pgclient.ChannelBinding;
import io.vertx.pgclient.impl.codec.ScramClientInitialMessage;
import io.vertx.pgclient.impl.util.Util;
-import javax.net.ssl.SSLException;
-import javax.net.ssl.SSLSession;
-import java.nio.charset.StandardCharsets;
-import java.security.cert.Certificate;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.List;
-
public class ScramSessionImpl implements ScramSession {
+ private static final Logger logger = LoggerFactory.getLogger(ScramSessionImpl.class);
+
private final String username;
private final char[] password;
+ private final ChannelBindingPolicy channelBinding;
private ScramClient scramClient;
- public ScramSessionImpl(String username, char[] password) {
+ public ScramSessionImpl(String username, char[] password, ChannelBinding channelBinding) {
this.username = username;
this.password = password;
+ this.channelBinding = getChannelBindingPolicy(channelBinding);
}
/*
* The client selects one of the supported mechanisms from the list,
- * and sends a SASLInitialResponse message to the server.
+ * and sends a SASLInitialResponse message to the server.
+ *
* The message includes the name of the selected mechanism, and
- * an optional Initial Client Response, if the selected mechanism uses that.
+ * an optional Initial Client Response, if the selected mechanism uses that.
*/
public ScramClientInitialMessage createInitialSaslMessage(ByteBuf in, ChannelHandlerContext ctx) {
List mechanisms = new ArrayList<>();
@@ -63,56 +77,64 @@ public ScramClientInitialMessage createInitialSaslMessage(ByteBuf in, ChannelHan
mechanisms.add(mechanism);
}
- if (mechanisms.isEmpty()) {
- throw new UnsupportedOperationException("SASL Authentication : the server returned no mechanism");
- }
+ logger.debug("Advertised SCRAM mechanisms: " + mechanisms);
- byte[] channelBindingData = extractChannelBindingData(ctx);
this.scramClient = ScramClient.builder()
.advertisedMechanisms(mechanisms)
.username(username) // ignored by the server, use startup message
.password(password)
.stringPreparation(StringPreparation.POSTGRESQL_PREPARATION)
- .channelBinding(TlsServerEndpoint.TLS_SERVER_END_POINT, channelBindingData)
+ .channelBindingPolicy(channelBinding)
+ .channelBinding(TlsServerEndpoint.TLS_SERVER_END_POINT, extractChannelBindingData(ctx))
.build();
- return new ScramClientInitialMessage(scramClient.clientFirstMessage().toString(),
+ logger.debug("Selected SCRAM mechanism: " + scramClient.getScramMechanism().getName());
+
+ String clientFirstMessage = scramClient.clientFirstMessage().toString();
+ logger.trace("SASLInitialResponse: " + clientFirstMessage);
+
+ return new ScramClientInitialMessage(clientFirstMessage,
scramClient.getScramMechanism().getName());
}
/*
* One or more server-challenge and client-response message will follow.
* Each server-challenge is sent in an AuthenticationSASLContinue message,
- * followed by a response from client in an SASLResponse message.
+ * followed by a response from client in an SASLResponse message.
* The particulars of the messages are mechanism specific.
*/
- public String receiveServerFirstMessage(ByteBuf in) {
+ public String receiveServerFirstMessage(ByteBuf in) {
String serverFirstMessage = in.readCharSequence(in.readableBytes(), StandardCharsets.UTF_8).toString();
+ logger.trace("AuthenticationSASLContinue: " + serverFirstMessage);
try {
scramClient.serverFirstMessage(serverFirstMessage);
} catch (ScramParseException e) {
- throw new UnsupportedOperationException(e);
+ throw new ScramRuntimeException(e.getMessage(), e);
}
- return scramClient.clientFinalMessage().toString();
+ String clientFinalMessage = scramClient.clientFinalMessage().toString();
+ logger.trace("SASLResponse: " + clientFinalMessage);
+
+ return clientFinalMessage;
}
/*
* Finally, when the authentication exchange is completed successfully,
- * the server sends an AuthenticationSASLFinal message, followed immediately by an AuthenticationOk message.
+ * the server sends an AuthenticationSASLFinal message, followed immediately by an AuthenticationOk message.
* The AuthenticationSASLFinal contains additional server-to-client data,
- * whose content is particular to the selected authentication mechanism.
+ * whose content is particular to the selected authentication mechanism.
* If the authentication mechanism doesn't use additional data that's sent at completion,
- * the AuthenticationSASLFinal message is not sent
+ * the AuthenticationSASLFinal message is not sent
*/
public void checkServerFinalMessage(ByteBuf in) {
String serverFinalMessage = in.readCharSequence(in.readableBytes(), StandardCharsets.UTF_8).toString();
+ logger.trace("AuthenticationSASLFinal: " + serverFinalMessage);
try {
scramClient.serverFinalMessage(serverFinalMessage);
} catch (ScramParseException | ScramServerErrorException | ScramInvalidServerSignatureException e) {
- throw new UnsupportedOperationException(e);
+ throw new ScramRuntimeException(e.getMessage(), e);
}
}
@@ -128,15 +150,31 @@ private byte[] extractChannelBindingData(ChannelHandlerContext ctx) {
Certificate peerCert = certificates[0]; // First certificate is the peer's certificate
if (peerCert instanceof X509Certificate) {
X509Certificate cert = (X509Certificate) peerCert;
- return TlsServerEndpoint.getChannelBindingData(cert);
+ return TlsServerEndpoint.getChannelBindingHash(cert);
}
}
- } catch (CertificateEncodingException | SSLException e) {
- // Cannot extract X509Certificate from SSL session
- // handle as no channel binding available
+ } catch (CertificateEncodingException | SSLException | NoSuchAlgorithmException e) {
+ logger.debug("Error extracting channel binding data", e);
+ if (channelBinding == ChannelBindingPolicy.REQUIRE) {
+ throw new ChannelBindingException(e.getMessage());
+ }
}
}
}
return new byte[0]; // handle as no channel binding available
}
+
+ private static ChannelBindingPolicy getChannelBindingPolicy(ChannelBinding channelBinding) {
+ logger.debug("ChannelBinding: " + channelBinding);
+ switch (channelBinding) {
+ case DISABLE:
+ return ChannelBindingPolicy.DISABLE;
+ case PREFER:
+ return ChannelBindingPolicy.ALLOW;
+ case REQUIRE:
+ return ChannelBindingPolicy.REQUIRE;
+ default:
+ throw new IllegalArgumentException("Invalid channel binding value");
+ }
+ }
}
diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/InitPgCommandMessage.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/InitPgCommandMessage.java
index 4f1c11bce..0126c2554 100644
--- a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/InitPgCommandMessage.java
+++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/InitPgCommandMessage.java
@@ -64,7 +64,8 @@ void handleAuthenticationSasl(ByteBuf in) {
// This will close the connection
throw new VertxException("Scram authentication not supported, missing com.ongres.scram:scram-client on the class/module path");
}
- scramSession = scramAuth.session(cmd.username(), cmd.password().toCharArray());
+ PgSocketConnection pgSocketConn = (PgSocketConnection) cmd.connection().unwrap();
+ scramSession = scramAuth.session(cmd.username(), cmd.password().toCharArray(), pgSocketConn.channelBinding());
encoder.writeScramClientInitialMessage(
scramSession.createInitialSaslMessage(in, encoder.channelHandlerContext()));
encoder.flush();
diff --git a/vertx-pg-client/src/test/java/io/vertx/tests/pgclient/PgConnectOptionsTest.java b/vertx-pg-client/src/test/java/io/vertx/tests/pgclient/PgConnectOptionsTest.java
index e6c5c7e15..797db8999 100644
--- a/vertx-pg-client/src/test/java/io/vertx/tests/pgclient/PgConnectOptionsTest.java
+++ b/vertx-pg-client/src/test/java/io/vertx/tests/pgclient/PgConnectOptionsTest.java
@@ -11,6 +11,7 @@
package io.vertx.tests.pgclient;
import io.vertx.core.json.JsonObject;
+import io.vertx.pgclient.ChannelBinding;
import io.vertx.pgclient.PgConnectOptions;
import io.vertx.pgclient.SslMode;
import io.vertx.pgclient.SslNegotiation;
@@ -167,6 +168,19 @@ public void testValidUriSslNegotiationDirect() {
assertEquals(expectedConfiguration, actualConfiguration);
}
+ @Test
+ public void testValidUriChannelBindingRequire() {
+ connectionUri = "postgresql://user@myhost?channel_binding=require";
+ actualConfiguration = PgConnectOptions.fromUri(connectionUri);
+
+ expectedConfiguration = new PgConnectOptions()
+ .setHost("myhost")
+ .setUser("user")
+ .setChannelBinding(ChannelBinding.REQUIRE);
+
+ assertEquals(expectedConfiguration, actualConfiguration);
+ }
+
@Test
public void testValidUri11() {
connectionUri = "postgresql://user@myhost?application_name=myapp";
diff --git a/vertx-pg-client/src/test/java/io/vertx/tests/pgclient/TLSTest.java b/vertx-pg-client/src/test/java/io/vertx/tests/pgclient/TLSTest.java
index 10448280a..63726a9ee 100644
--- a/vertx-pg-client/src/test/java/io/vertx/tests/pgclient/TLSTest.java
+++ b/vertx-pg-client/src/test/java/io/vertx/tests/pgclient/TLSTest.java
@@ -20,10 +20,12 @@
import io.vertx.ext.unit.Async;
import io.vertx.ext.unit.TestContext;
import io.vertx.ext.unit.junit.VertxUnitRunner;
+import io.vertx.pgclient.ChannelBinding;
import io.vertx.pgclient.PgConnectOptions;
import io.vertx.pgclient.PgConnection;
import io.vertx.pgclient.SslMode;
import io.vertx.pgclient.SslNegotiation;
+import io.vertx.sqlclient.ClosedConnectionException;
import io.vertx.sqlclient.Tuple;
import io.vertx.tests.pgclient.junit.ContainerPgRule;
import org.junit.*;
@@ -261,4 +263,33 @@ public void testSslNegotiationDirectWithVerifyFull(TestContext ctx) {
async.complete();
}));
}
+
+ @Test
+ public void testChannelBindingRequireWithSsl(TestContext ctx) {
+ Async async = ctx.async();
+ PgConnectOptions options = new PgConnectOptions(ruleOptionalSll.options())
+ .setSslMode(SslMode.REQUIRE)
+ .setChannelBinding(ChannelBinding.REQUIRE)
+ .setSslOptions(new ClientSSLOptions().setTrustAll(true));
+
+ PgConnection.connect(vertx, options).onComplete(ctx.asyncAssertSuccess(conn -> {
+ ctx.assertTrue(conn.isSSL());
+ async.complete();
+ }));
+ }
+
+ @Test
+ public void testChannelBindingRequireWithoutSsl(TestContext ctx) {
+ Async async = ctx.async();
+ PgConnectOptions options = new PgConnectOptions(ruleOptionalSll.options())
+ .setSslMode(SslMode.DISABLE)
+ .setChannelBinding(ChannelBinding.REQUIRE)
+ .setSslOptions(new ClientSSLOptions().setTrustAll(true));
+
+ PgConnection.connect(vertx, options).onComplete(ctx.asyncAssertFailure(err -> {
+ // ctx.assertEquals("Channel bindins is required", err.getMessage());
+ ctx.assertTrue(err instanceof ClosedConnectionException); // TODO: handle ChannelBindingException
+ async.complete();
+ }));
+ }
}