Skip to content

Commit 86caa70

Browse files
committed
feat: support channel binding configuration in PgConnectOptions
Introduce support for configuring channel binding mechanisms during the SCRAM-SHA-256-PLUS authentication handshake. This allows users to enforce, prefer, or disable channel binding securely. - Add channelBinding property and getters/setters to PgConnectOptions - Update PgConnectionUriParser to parse 'channel_binding' from connection URIs - Store channel binding preference in Netty Channel attributes during initialization - Propagate the configuration to ScramAuthentication to negotiate -PLUS variants - Fail the connection handshake if a mismatch or unsupported negotiation occurs Signed-off-by: Jorge Solorzano <jorsol@gmail.com>
1 parent 5869727 commit 86caa70

10 files changed

Lines changed: 254 additions & 30 deletions

File tree

vertx-pg-client/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
<dependency>
5252
<groupId>com.ongres.scram</groupId>
5353
<artifactId>scram-client</artifactId>
54-
<version>3.2</version>
54+
<version>3.4</version>
5555
</dependency>
5656

5757
<!-- Testing purposes -->

vertx-pg-client/src/main/asciidoc/index.adoc

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ Currently, the client supports the following parameter keys:
195195
* `dbname`
196196
* `sslmode`
197197
* `sslnegotiation`
198+
* `channel_binding`
198199

199200
Additional parameters will be added to the {@link io.vertx.sqlclient.SqlConnectOptions#getProperties properties}.
200201

@@ -214,6 +215,7 @@ for more details. The following parameters are supported:
214215
* `PGPASSWORD`
215216
* `PGSSLMODE`
216217
* `PGSSLNEGOTIATION`
218+
* `PGCHANNELBINDING`
217219

218220
If you don't specify a data object or a connection URI string to connect, environment variables will take precedence over them.
219221

@@ -224,7 +226,8 @@ $ PGUSER=user \
224226
PGPASSWORD=secret \
225227
PGDATABASE=the-db \
226228
PGPORT=5432 \
227-
PGSSLMODE=DISABLE
229+
PGSSLMODE=DISABLE \
230+
PGCHANNELBINDING=prefer
228231
----
229232

230233
[source,$lang]
@@ -767,6 +770,36 @@ Attempting to use `DIRECT` mode with older PostgreSQL versions will result in an
767770

768771
More information can be found in the http://vertx.io/docs/vertx-core/java/#ssl[Vert.x documentation].
769772

773+
=== Channel Binding (SCRAM authentication)
774+
775+
Channel binding is a method for the server to authenticate itself to the client.
776+
It is only supported over SSL connections with PostgreSQL 11 or later servers using the `SCRAM authentication` method.
777+
778+
The client supports three channel binding modes via {@link io.vertx.pgclient.ChannelBinding}:
779+
780+
- `DISABLE`: Prevents the use of channel binding
781+
- `PREFER` (default): Means that the client will choose channel binding if available
782+
- `REQUIRE`: Means that the connection must employ channel binding
783+
784+
[source,$lang]
785+
----
786+
{@link examples.PgClientExamples#channelBinding}
787+
----
788+
789+
You can also configure Channel binding via connection URI:
790+
791+
[source,$lang]
792+
----
793+
{@link examples.PgClientExamples#channelBindingUri}
794+
----
795+
796+
Or using the environment variable:
797+
798+
[source,bash]
799+
----
800+
export PGCHANNELBINDING=require
801+
----
802+
770803
== Using a level 4 proxy
771804

772805
You can configure the client to use an HTTP/1.x CONNECT, SOCKS4a or SOCKS5 level 4 proxy.

vertx-pg-client/src/main/java/examples/PgClientExamples.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,33 @@ public void sslNegotiationUri() {
539539
PgConnectOptions options = PgConnectOptions.fromUri(connectionUri);
540540
}
541541

542+
public void channelBinding(Vertx vertx) {
543+
PgConnectOptions options = new PgConnectOptions()
544+
.setPort(5432)
545+
.setHost("the-host")
546+
.setDatabase("the-db")
547+
.setUser("user")
548+
.setPassword("secret")
549+
.setSslMode(SslMode.REQUIRE)
550+
.setChannelBinding(ChannelBinding.REQUIRE)
551+
.setSslOptions(new ClientSSLOptions()
552+
.setTrustOptions(new PemTrustOptions().addCertPath("/path/to/server.crt")));
553+
554+
PgConnection.connect(vertx, options)
555+
.onComplete(res -> {
556+
if (res.succeeded()) {
557+
System.out.println("Connected with channel binding enforcement");
558+
} else {
559+
System.out.println("Could not connect: " + res.cause().getMessage());
560+
}
561+
});
562+
}
563+
564+
public void channelBindingUri() {
565+
String connectionUri = "postgresql://localhost/mydb?sslmode=require&channel_binding=require";
566+
PgConnectOptions options = PgConnectOptions.fromUri(connectionUri);
567+
}
568+
542569
public void jsonExample() {
543570

544571
// Create a tuple
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright (c) 2026 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*/
11+
package io.vertx.pgclient;
12+
13+
import io.vertx.codegen.annotations.VertxGen;
14+
15+
/**
16+
* The different values for the Channel Binding parameter provide different levels of
17+
* protection. Channel binding is a method for the server to authenticate itself to the client.
18+
* It is only supported over SSL connections with PostgreSQL 11 or later servers using the
19+
* SCRAM authentication method.
20+
*
21+
* @see <a href=
22+
* "https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-CHANNEL-BINDING">
23+
* libpq channel_binding</a>
24+
*/
25+
@VertxGen
26+
public enum ChannelBinding {
27+
28+
/**
29+
* Prevents the use of channel binding
30+
*/
31+
DISABLE("disable"),
32+
33+
/**
34+
* Means that the client will choose channel binding if available.
35+
*/
36+
PREFER("prefer"),
37+
38+
/**
39+
* Means that the connection must employ channel binding.
40+
*/
41+
REQUIRE("require");
42+
43+
public static final ChannelBinding[] VALUES = ChannelBinding.values();
44+
45+
public final String value;
46+
47+
ChannelBinding(String value) {
48+
this.value = value;
49+
}
50+
51+
public static ChannelBinding of(String value) {
52+
for (ChannelBinding channelBinding : VALUES) {
53+
if (channelBinding.value.equalsIgnoreCase(value)) {
54+
return channelBinding;
55+
}
56+
}
57+
58+
throw new IllegalArgumentException("Could not find an appropriate Channel Binding mode for the value [" + value + "].");
59+
}
60+
}

vertx-pg-client/src/main/java/io/vertx/pgclient/PgConnectOptions.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ public static PgConnectOptions fromEnv() {
9999
if (getenv("PGSSLNEGOTIATION") != null) {
100100
pgConnectOptions.setSslNegotiation(SslNegotiation.of(getenv("PGSSLNEGOTIATION")));
101101
}
102+
if (getenv("PGCHANNELBINDING") != null) {
103+
pgConnectOptions.setChannelBinding(ChannelBinding.of(getenv("PGCHANNELBINDING")));
104+
}
102105
return pgConnectOptions;
103106
}
104107

@@ -110,6 +113,7 @@ public static PgConnectOptions fromEnv() {
110113
public static final int DEFAULT_PIPELINING_LIMIT = 256;
111114
public static final SslMode DEFAULT_SSLMODE = SslMode.DISABLE;
112115
public static final SslNegotiation DEFAULT_SSL_NEGOTIATION = SslNegotiation.POSTGRES;
116+
public static final ChannelBinding DEFAULT_CHANNEL_BINDING = ChannelBinding.PREFER;
113117
public static final boolean DEFAULT_USE_LAYER_7_PROXY = false;
114118
public static final Map<String, String> DEFAULT_PROPERTIES;
115119

@@ -125,6 +129,7 @@ public static PgConnectOptions fromEnv() {
125129
private int pipeliningLimit = DEFAULT_PIPELINING_LIMIT;
126130
private SslMode sslMode = DEFAULT_SSLMODE;
127131
private SslNegotiation sslNegotiation = DEFAULT_SSL_NEGOTIATION;
132+
private ChannelBinding channelBinding = DEFAULT_CHANNEL_BINDING;
128133
private boolean useLayer7Proxy = DEFAULT_USE_LAYER_7_PROXY;
129134

130135
public PgConnectOptions() {
@@ -143,6 +148,7 @@ public PgConnectOptions(SqlConnectOptions other) {
143148
pipeliningLimit = opts.pipeliningLimit;
144149
sslMode = opts.sslMode;
145150
sslNegotiation = opts.sslNegotiation;
151+
channelBinding = opts.channelBinding;
146152
}
147153
}
148154

@@ -151,6 +157,7 @@ public PgConnectOptions(PgConnectOptions other) {
151157
pipeliningLimit = other.pipeliningLimit;
152158
sslMode = other.sslMode;
153159
sslNegotiation = other.sslNegotiation;
160+
channelBinding = other.channelBinding;
154161
}
155162

156163
@Override
@@ -258,6 +265,24 @@ public PgConnectOptions setSslNegotiation(SslNegotiation sslNegotiation) {
258265
return this;
259266
}
260267

268+
/**
269+
* @return the value of current Channel Binding mode
270+
*/
271+
public ChannelBinding getChannelBinding() {
272+
return channelBinding;
273+
}
274+
275+
/**
276+
* Set {@link ChannelBinding} for the client, this option controls the client's use of channel binding.
277+
*
278+
* @param channelBinding the channel binding mode
279+
* @return a reference to this, so the API can be used fluently
280+
*/
281+
public PgConnectOptions setChannelBinding(ChannelBinding channelBinding) {
282+
this.channelBinding = channelBinding;
283+
return this;
284+
}
285+
261286
/**
262287
* @return whether the client interacts with a layer 7 proxy instead of a server
263288
*/
@@ -342,6 +367,7 @@ public boolean equals(Object o) {
342367
if (pipeliningLimit != that.pipeliningLimit) return false;
343368
if (sslMode != that.sslMode) return false;
344369
if (sslNegotiation != that.sslNegotiation) return false;
370+
if (channelBinding != that.channelBinding) return false;
345371

346372
return true;
347373
}
@@ -352,6 +378,7 @@ public int hashCode() {
352378
result = 31 * result + pipeliningLimit;
353379
result = 31 * result + sslMode.hashCode();
354380
result = 31 * result + sslNegotiation.hashCode();
381+
result = 31 * result + channelBinding.hashCode();
355382
return result;
356383
}
357384

vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgConnectionUriParser.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
package io.vertx.pgclient.impl;
1212

1313
import io.vertx.core.json.JsonObject;
14+
import io.vertx.pgclient.ChannelBinding;
1415
import io.vertx.pgclient.SslMode;
1516
import io.vertx.pgclient.SslNegotiation;
1617

@@ -183,6 +184,9 @@ private static void parseParameters(String parametersInfo, JsonObject configurat
183184
case "sslnegotiation":
184185
configuration.put("sslNegotiation", SslNegotiation.of(value));
185186
break;
187+
case "channel_binding":
188+
configuration.put("channelBinding", ChannelBinding.of(value));
189+
break;
186190
default:
187191
properties.put(key, value);
188192
break;

vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgSocketConnection.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import io.netty.channel.ChannelPipeline;
2121
import io.netty.handler.codec.DecoderException;
22+
import io.netty.util.AttributeKey;
2223
import io.vertx.core.*;
2324
import io.vertx.core.buffer.Buffer;
2425
import io.vertx.core.net.ClientSSLOptions;
@@ -81,6 +82,11 @@ protected PgConnectOptions connectOptions() {
8182
@Override
8283
public void init() {
8384
codec = new PgCodec(useLayer7Proxy);
85+
86+
socket.channelHandlerContext().channel()
87+
.attr(AttributeKey.valueOf("channel_binding"))
88+
.set(connectOptions.getChannelBinding());
89+
8490
ChannelPipeline pipeline = socket.channelHandlerContext().pipeline();
8591
pipeline.addBefore("handler", "codec", codec);
8692
super.init();

0 commit comments

Comments
 (0)