Skip to content

Commit 9018502

Browse files
jorsoltsegismont
authored andcommitted
Update dependency SCRAM 3 and support channel binding
Also, the scram dependency is optional and the module descriptor declares it as static, however in practice when the scram dependency is not available the client cannot be used because the ScramAuthentication is unavailable due to a classloading error. The client should be resilient to this common case and continue to work when scram is not available. Changes: Introduce an indirection level, the scram interactions are now handled by a ScramSession and the ScramAuthentication is a factory for sessions. When the scram dependency is not available the ScramAuthentication instance is null, the InitCommandCodec can check this field and throw an exception that will close the connection (as per the protocol requirements). Result: The client runs fine without the scram dependency on the class/module path. Signed-off-by: Thomas Segismont <[email protected]>
1 parent f821920 commit 9018502

File tree

8 files changed

+239
-63
lines changed

8 files changed

+239
-63
lines changed

vertx-pg-client/pom.xml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@
6464
<!-- sasl scram authentication -->
6565
<dependency>
6666
<groupId>com.ongres.scram</groupId>
67-
<artifactId>client</artifactId>
68-
<version>2.1</version>
67+
<artifactId>scram-client</artifactId>
68+
<version>3.2</version>
6969
<optional>true</optional>
7070
</dependency>
7171

@@ -196,6 +196,21 @@
196196
</environmentVariables>
197197
</configuration>
198198
</execution>
199+
<execution>
200+
<id>missing-scram-test</id>
201+
<goals>
202+
<goal>integration-test</goal>
203+
</goals>
204+
<phase>integration-test</phase>
205+
<configuration>
206+
<includes>
207+
<include>io/vertx/pgclient/it/MissingScramTest.java</include>
208+
</includes>
209+
<classpathDependencyExcludes>
210+
<classpathDependencyExclude>com.ongres.scram:scram-client</classpathDependencyExclude>
211+
</classpathDependencyExcludes>
212+
</configuration>
213+
</execution>
199214
</executions>
200215
</plugin>
201216
</plugins>

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -233,29 +233,28 @@ $ PGUSER=user \
233233

234234
=== SASL SCRAM-SHA-256 authentication mechanism.
235235

236-
To use the sasl SCRAM-SHA-256 authentication add the following dependency to the _dependencies_ section of your build descriptor:
236+
To use the SASL `SCRAM-SHA-256` authentication add the following dependency to the _dependencies_ section of your build descriptor:
237237

238238
* Maven (in your `pom.xml`):
239239

240240
[source,xml]
241241
----
242242
<dependency>
243243
<groupId>com.ongres.scram</groupId>
244-
<artifactId>client</artifactId>
245-
<version>2.1</version>
244+
<artifactId>scram-client</artifactId>
245+
<version>3.1</version>
246246
</dependency>
247247
----
248248
* Gradle (in your `build.gradle` file):
249249

250250
[source,groovy]
251251
----
252252
dependencies {
253-
compile 'com.ongres.scram:client:2.1'
253+
compile 'com.ongres.scram:scram-client:3.1'
254254
}
255255
----
256256

257-
NOTE: SCRAM-SHA-256-PLUS (added in Postgresql 11) is not supported.
258-
257+
When the database requires a scram authentication and the scram client jar is not on the class/module path, the connection will be closed by the client.
259258

260259
include::queries.adoc[leveloffset=1]
261260

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package io.vertx.pgclient.impl.auth.scram;
2+
3+
import com.ongres.scram.client.ScramClient;
4+
import io.vertx.core.impl.logging.Logger;
5+
import io.vertx.core.impl.logging.LoggerFactory;
6+
7+
public class ScramAuthentication {
8+
9+
private static final Logger logger = LoggerFactory.getLogger(ScramAuthentication.class);
10+
11+
public static ScramAuthentication INSTANCE;
12+
13+
static {
14+
ScramAuthentication instance;
15+
try {
16+
ScramClient.MechanismsBuildStage builder = ScramClient.builder();
17+
logger.debug("Scram authentication is available " + builder);
18+
instance = new ScramAuthentication();
19+
} catch (Throwable notFound) {
20+
instance = null;
21+
}
22+
INSTANCE = instance;
23+
}
24+
25+
private ScramAuthentication() {
26+
}
27+
28+
public ScramSession session(String username, char[] password) {
29+
return new ScramSessionImpl(username, password);
30+
}
31+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright (C) 2017 Julien Viet
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package io.vertx.pgclient.impl.auth.scram;
19+
20+
import io.netty.buffer.ByteBuf;
21+
import io.netty.channel.ChannelHandlerContext;
22+
import io.vertx.pgclient.impl.codec.ScramClientInitialMessage;
23+
24+
public interface ScramSession {
25+
26+
/*
27+
* The client selects one of the supported mechanisms from the list,
28+
* and sends a SASLInitialResponse message to the server.
29+
* The message includes the name of the selected mechanism, and
30+
* an optional Initial Client Response, if the selected mechanism uses that.
31+
*/
32+
ScramClientInitialMessage createInitialSaslMessage(ByteBuf in, ChannelHandlerContext ctx);
33+
34+
/*
35+
* One or more server-challenge and client-response message will follow.
36+
* Each server-challenge is sent in an AuthenticationSASLContinue message,
37+
* followed by a response from client in an SASLResponse message.
38+
* The particulars of the messages are mechanism specific.
39+
*/
40+
String receiveServerFirstMessage(ByteBuf in);
41+
42+
/*
43+
* Finally, when the authentication exchange is completed successfully,
44+
* the server sends an AuthenticationSASLFinal message, followed immediately by an AuthenticationOk message.
45+
* The AuthenticationSASLFinal contains additional server-to-client data,
46+
* whose content is particular to the selected authentication mechanism.
47+
* If the authentication mechanism doesn't use additional data that's sent at completion,
48+
* the AuthenticationSASLFinal message is not sent
49+
*/
50+
void checkServerFinalMessage(ByteBuf in);
51+
52+
}

vertx-pg-client/src/main/java/io/vertx/pgclient/impl/util/ScramAuthentication.java renamed to vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramSessionImpl.java

Lines changed: 60 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,34 +15,36 @@
1515
*
1616
*/
1717

18-
package io.vertx.pgclient.impl.util;
19-
20-
import java.nio.charset.StandardCharsets;
21-
import java.util.ArrayList;
22-
import java.util.List;
18+
package io.vertx.pgclient.impl.auth.scram;
2319

2420
import com.ongres.scram.client.ScramClient;
25-
import com.ongres.scram.client.ScramSession;
26-
import com.ongres.scram.common.exception.ScramException;
21+
import com.ongres.scram.common.StringPreparation;
2722
import com.ongres.scram.common.exception.ScramInvalidServerSignatureException;
2823
import com.ongres.scram.common.exception.ScramParseException;
2924
import com.ongres.scram.common.exception.ScramServerErrorException;
30-
import com.ongres.scram.common.stringprep.StringPreparations;
31-
25+
import com.ongres.scram.common.util.TlsServerEndpoint;
3226
import io.netty.buffer.ByteBuf;
27+
import io.netty.channel.ChannelHandlerContext;
28+
import io.netty.handler.ssl.SslHandler;
3329
import io.vertx.pgclient.impl.codec.ScramClientInitialMessage;
30+
import io.vertx.pgclient.impl.util.Util;
3431

35-
public class ScramAuthentication {
32+
import javax.net.ssl.SSLException;
33+
import javax.net.ssl.SSLSession;
34+
import java.nio.charset.StandardCharsets;
35+
import java.security.cert.Certificate;
36+
import java.security.cert.CertificateEncodingException;
37+
import java.security.cert.X509Certificate;
38+
import java.util.ArrayList;
39+
import java.util.List;
3640

37-
private static final String SCRAM_SHA_256 = "SCRAM-SHA-256";
41+
public class ScramSessionImpl implements ScramSession {
3842

3943
private final String username;
40-
private final String password;
41-
private ScramSession scramSession;
42-
private ScramSession.ClientFinalProcessor clientFinalProcessor;
44+
private final char[] password;
45+
private ScramClient scramClient;
4346

44-
45-
public ScramAuthentication(String username, String password) {
47+
public ScramSessionImpl(String username, char[] password) {
4648
this.username = username;
4749
this.password = password;
4850
}
@@ -53,39 +55,31 @@ public ScramAuthentication(String username, String password) {
5355
* The message includes the name of the selected mechanism, and
5456
* an optional Initial Client Response, if the selected mechanism uses that.
5557
*/
56-
public ScramClientInitialMessage createInitialSaslMessage(ByteBuf in) {
58+
public ScramClientInitialMessage createInitialSaslMessage(ByteBuf in, ChannelHandlerContext ctx) {
5759
List<String> mechanisms = new ArrayList<>();
5860

5961
while (0 != in.getByte(in.readerIndex())) {
60-
String mechanism = Util.readCStringUTF8(in);
61-
mechanisms.add(mechanism);
62+
String mechanism = Util.readCStringUTF8(in);
63+
mechanisms.add(mechanism);
6264
}
6365

6466
if (mechanisms.isEmpty()) {
6567
throw new UnsupportedOperationException("SASL Authentication : the server returned no mechanism");
6668
}
6769

68-
// SCRAM-SHA-256-PLUS added in postgresql 11 is not supported
69-
if (!mechanisms.contains(SCRAM_SHA_256)) {
70-
throw new UnsupportedOperationException("SASL Authentication : only SCRAM-SHA-256 is currently supported, server wants " + mechanisms);
71-
}
72-
73-
74-
ScramClient scramClient = ScramClient
75-
.channelBinding(ScramClient.ChannelBinding.NO)
76-
.stringPreparation(StringPreparations.NO_PREPARATION)
77-
.selectMechanismBasedOnServerAdvertised(mechanisms.toArray(new String[0]))
78-
.setup();
79-
80-
81-
// this user name will be ignored, the user name that was already sent in the startup message is used instead
82-
// see https://www.postgresql.org/docs/11/sasl-authentication.html#SASL-SCRAM-SHA-256 §53.3.1
83-
scramSession = scramClient.scramSession(this.username);
84-
85-
return new ScramClientInitialMessage(scramSession.clientFirstMessage(), scramClient.getScramMechanism().getName());
70+
byte[] channelBindingData = extractChannelBindingData(ctx);
71+
this.scramClient = ScramClient.builder()
72+
.advertisedMechanisms(mechanisms)
73+
.username(username) // ignored by the server, use startup message
74+
.password(password)
75+
.stringPreparation(StringPreparation.POSTGRESQL_PREPARATION)
76+
.channelBinding(TlsServerEndpoint.TLS_SERVER_END_POINT, channelBindingData)
77+
.build();
78+
79+
return new ScramClientInitialMessage(scramClient.clientFirstMessage().toString(),
80+
scramClient.getScramMechanism().getName());
8681
}
8782

88-
8983
/*
9084
* One or more server-challenge and client-response message will follow.
9185
* Each server-challenge is sent in an AuthenticationSASLContinue message,
@@ -95,16 +89,13 @@ public ScramClientInitialMessage createInitialSaslMessage(ByteBuf in) {
9589
public String receiveServerFirstMessage(ByteBuf in) {
9690
String serverFirstMessage = in.readCharSequence(in.readableBytes(), StandardCharsets.UTF_8).toString();
9791

98-
ScramSession.ServerFirstProcessor serverFirstProcessor = null;
9992
try {
100-
serverFirstProcessor = scramSession.receiveServerFirstMessage(serverFirstMessage);
101-
} catch (ScramException e) {
93+
scramClient.serverFirstMessage(serverFirstMessage);
94+
} catch (ScramParseException e) {
10295
throw new UnsupportedOperationException(e);
10396
}
10497

105-
clientFinalProcessor = serverFirstProcessor.clientFinalProcessor(password);
106-
107-
return clientFinalProcessor.clientFinalMessage();
98+
return scramClient.clientFinalMessage().toString();
10899
}
109100

110101
/*
@@ -119,9 +110,33 @@ public void checkServerFinalMessage(ByteBuf in) {
119110
String serverFinalMessage = in.readCharSequence(in.readableBytes(), StandardCharsets.UTF_8).toString();
120111

121112
try {
122-
clientFinalProcessor.receiveServerFinalMessage(serverFinalMessage);
113+
scramClient.serverFinalMessage(serverFinalMessage);
123114
} catch (ScramParseException | ScramServerErrorException | ScramInvalidServerSignatureException e) {
124115
throw new UnsupportedOperationException(e);
125116
}
126117
}
118+
119+
private byte[] extractChannelBindingData(ChannelHandlerContext ctx) {
120+
SslHandler sslHandler = ctx.channel().pipeline().get(SslHandler.class);
121+
if (sslHandler != null) {
122+
SSLSession sslSession = sslHandler.engine().getSession();
123+
if (sslSession != null && sslSession.isValid()) {
124+
try {
125+
// Get the certificate chain from the session
126+
Certificate[] certificates = sslSession.getPeerCertificates();
127+
if (certificates != null && certificates.length > 0) {
128+
Certificate peerCert = certificates[0]; // First certificate is the peer's certificate
129+
if (peerCert instanceof X509Certificate) {
130+
X509Certificate cert = (X509Certificate) peerCert;
131+
return TlsServerEndpoint.getChannelBindingData(cert);
132+
}
133+
}
134+
} catch (CertificateEncodingException | SSLException e) {
135+
// Cannot extract X509Certificate from SSL session
136+
// handle as no channel binding available
137+
}
138+
}
139+
}
140+
return new byte[0]; // handle as no channel binding available
141+
}
127142
}

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

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,24 @@
1616
*/
1717
package io.vertx.pgclient.impl.codec;
1818

19-
import java.nio.charset.Charset;
20-
import java.nio.charset.StandardCharsets;
21-
2219
import io.netty.buffer.ByteBuf;
20+
import io.vertx.core.VertxException;
2321
import io.vertx.pgclient.impl.PgDatabaseMetadata;
2422
import io.vertx.pgclient.impl.PgSocketConnection;
25-
import io.vertx.pgclient.impl.util.ScramAuthentication;
23+
import io.vertx.pgclient.impl.auth.scram.ScramAuthentication;
24+
import io.vertx.pgclient.impl.auth.scram.ScramSession;
2625
import io.vertx.sqlclient.impl.Connection;
2726
import io.vertx.sqlclient.impl.command.CommandResponse;
2827
import io.vertx.sqlclient.impl.command.InitCommand;
2928

29+
import java.nio.charset.Charset;
30+
import java.nio.charset.StandardCharsets;
31+
3032
class InitCommandCodec extends PgCommandCodec<Connection, InitCommand> {
3133

3234
private PgEncoder encoder;
3335
private String encoding;
34-
private ScramAuthentication scramAuthentication;
36+
private ScramSession scramSession;
3537

3638
InitCommandCodec(InitCommand cmd) {
3739
super(cmd);
@@ -57,20 +59,26 @@ public void handleAuthenticationClearTextPassword() {
5759

5860
@Override
5961
void handleAuthenticationSasl(ByteBuf in) {
60-
scramAuthentication = new ScramAuthentication(cmd.username(), cmd.password());
61-
encoder.writeScramClientInitialMessage(scramAuthentication.createInitialSaslMessage(in));
62+
ScramAuthentication scramAuth = ScramAuthentication.INSTANCE;
63+
if (scramAuth == null) {
64+
// This will close the connection
65+
throw new VertxException("Scram authentication not supported, missing com.ongres.scram:scram-client on the class/module path");
66+
}
67+
scramSession = scramAuth.session(cmd.username(), cmd.password().toCharArray());
68+
encoder.writeScramClientInitialMessage(
69+
scramSession.createInitialSaslMessage(in, encoder.channelHandlerContext()));
6270
encoder.flush();
6371
}
6472

6573
@Override
6674
void handleAuthenticationSaslContinue(ByteBuf in) {
67-
encoder.writeScramClientFinalMessage(new ScramClientFinalMessage(scramAuthentication.receiveServerFirstMessage(in)));
75+
encoder.writeScramClientFinalMessage(new ScramClientFinalMessage(scramSession.receiveServerFirstMessage(in)));
6876
encoder.flush();
6977
}
7078

7179
@Override
7280
void handleAuthenticationSaslFinal(ByteBuf in) {
73-
scramAuthentication.checkServerFinalMessage(in);
81+
scramSession.checkServerFinalMessage(in);
7482
}
7583

7684
@Override

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import io.vertx.sqlclient.impl.RowDesc;
3131
import io.vertx.sqlclient.impl.command.*;
3232

33-
import java.util.ArrayDeque;
3433
import java.util.Map;
3534

3635
import static io.vertx.pgclient.impl.util.Util.writeCString;

0 commit comments

Comments
 (0)