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(); + })); + } }