Skip to content

Commit

Permalink
Update SCRAM dependency to 3.0 and support channel binding
Browse files Browse the repository at this point in the history
Signed-off-by: Jorge Solórzano <jorsol@gmail.com>
  • Loading branch information
jorsol committed Apr 8, 2024
1 parent 0fbd31d commit 84e538b
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 236 deletions.
12 changes: 8 additions & 4 deletions pgjdbc/build.gradle.kts
Expand Up @@ -84,7 +84,7 @@ dependencies {
"testImplementation"("org.osgi:org.osgi.service.jdbc") {
because("DataSourceFactory is needed for PGDataSourceFactoryTest")
}
shaded("com.ongres.scram:client:2.1")
shaded("com.ongres.scram:scram-client:3.0")

implementation("org.checkerframework:checker-qual:3.42.0")
testImplementation("se.jiderhamn:classloader-leak-test-framework:1.1.2")
Expand Down Expand Up @@ -188,10 +188,10 @@ tasks.compileJava {
val getShadedDependencyLicenses by tasks.registering(GatherLicenseTask::class) {
configuration(shaded)
extraLicenseDir.set(file("$rootDir/licenses"))
overrideLicense("com.ongres.scram:common") {
overrideLicense("com.ongres.scram:scram-common") {
licenseFiles = "scram"
}
overrideLicense("com.ongres.scram:client") {
overrideLicense("com.ongres.scram:scram-client") {
licenseFiles = "scram"
}
overrideLicense("com.ongres.stringprep:saslprep") {
Expand Down Expand Up @@ -224,6 +224,10 @@ tasks.configureEach<Jar> {
tasks.shadowJar {
configurations = listOf(shaded)
exclude("META-INF/maven/**")
// ignore module-info.class not used in shaded dependency
exclude("META-INF/versions/9/module-info.class")
// ignore service file not used in shaded dependency
exclude("META-INF/services/com.ongres.stringprep.Profile")
exclude("META-INF/LICENSE*")
exclude("META-INF/NOTICE*")
into("META-INF") {
Expand Down Expand Up @@ -251,7 +255,7 @@ val osgiJar by tasks.registering(Bundle::class) {
Bundle-Activator: org.postgresql.osgi.PGBundleActivator
Bundle-SymbolicName: org.postgresql.jdbc
Bundle-Name: PostgreSQL JDBC Driver
Bundle-Copyright: Copyright (c) 2003-2020, PostgreSQL Global Development Group
Bundle-Copyright: Copyright (c) 2003-2024, PostgreSQL Global Development Group
Require-Capability: osgi.ee;filter:="(&(|(osgi.ee=J2SE)(osgi.ee=JavaSE))(version>=1.8))"
Provide-Capability: osgi.service;effective:=active;objectClass=org.osgi.service.jdbc.DataSourceFactory;osgi.jdbc.driver.class=org.postgresql.Driver;osgi.jdbc.driver.name=PostgreSQL JDBC Driver
"""
Expand Down
4 changes: 2 additions & 2 deletions pgjdbc/reduced-pom.xml
Expand Up @@ -43,8 +43,8 @@
<dependencies>
<dependency>
<groupId>com.ongres.scram</groupId>
<artifactId>client</artifactId>
<version>%{com.ongres.scram:client}</version>
<artifactId>scram-client</artifactId>
<version>%{com.ongres.scram:scram-client}</version>
</dependency>
<dependency>
<groupId>se.jiderhamn</groupId>
Expand Down
Expand Up @@ -27,7 +27,6 @@
import org.postgresql.hostchooser.HostStatus;
import org.postgresql.jdbc.GSSEncMode;
import org.postgresql.jdbc.SslMode;
import org.postgresql.jre7.sasl.ScramAuthenticator;
import org.postgresql.plugin.AuthenticationRequestType;
import org.postgresql.ssl.MakeSSL;
import org.postgresql.sspi.ISSPIClient;
Expand Down Expand Up @@ -835,8 +834,6 @@ private void doAuthentication(PGStream pgStream, String host, String user, Prope
break;

case AUTH_REQ_SASL:
LOGGER.log(Level.FINEST, " <=BE AuthenticationSASL");

scramAuthenticator = AuthenticationPluginManager.withPassword(AuthenticationRequestType.SASL, info, password -> {
if (password == null) {
throw new PSQLException(
Expand All @@ -850,26 +847,17 @@ private void doAuthentication(PGStream pgStream, String host, String user, Prope
"The server requested SCRAM-based authentication, but the password is an empty string."),
PSQLState.CONNECTION_REJECTED);
}
return new ScramAuthenticator(user, String.valueOf(password), pgStream);
return new ScramAuthenticator(password, pgStream);
});
scramAuthenticator.processServerMechanismsAndInit();
scramAuthenticator.sendScramClientFirstMessage();
// This works as follows:
// 1. When tests is run from IDE, it is assumed SCRAM library is on the classpath
// 2. In regular build for Java < 8 this `if` is deactivated and the code always throws
if (false) {
throw new PSQLException(GT.tr(
"SCRAM authentication is not supported by this driver. You need JDK >= 8 and pgjdbc >= 42.2.0 (not \".jre\" versions)",
areq), PSQLState.CONNECTION_REJECTED);
}
scramAuthenticator.handleAuthenticationSASL();
break;

case AUTH_REQ_SASL_CONTINUE:
castNonNull(scramAuthenticator).processServerFirstMessage(msgLen - 4 - 4);
castNonNull(scramAuthenticator).handleAuthenticationSASLContinue(msgLen - 4 - 4);
break;

case AUTH_REQ_SASL_FINAL:
castNonNull(scramAuthenticator).verifyServerSignature(msgLen - 4 - 4);
castNonNull(scramAuthenticator).handleAuthenticationSASLFinal(msgLen - 4 - 4);
break;

case AUTH_REQ_OK:
Expand Down
172 changes: 172 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/core/v3/ScramAuthenticator.java
@@ -0,0 +1,172 @@
/*
* Copyright (c) 2017, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/

package org.postgresql.core.v3;

import org.postgresql.core.PGStream;
import org.postgresql.util.GT;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;

import com.ongres.scram.client.ScramClient;
import com.ongres.scram.common.ClientFinalMessage;
import com.ongres.scram.common.ClientFirstMessage;
import com.ongres.scram.common.StringPreparation;
import com.ongres.scram.common.exception.ScramException;
import com.ongres.scram.common.util.TlsServerEndpoint;

import java.io.IOException;
import java.net.Socket;
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;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;

final class ScramAuthenticator {
private static final Logger LOGGER = Logger.getLogger(ScramAuthenticator.class.getName());

private final PGStream pgStream;
private final ScramClient scramClient;

ScramAuthenticator(char[] password, PGStream pgStream) throws PSQLException {
this.pgStream = pgStream;
this.scramClient = initializeScramClient(password, pgStream);
}

private static ScramClient initializeScramClient(char[] password, PGStream stream) throws PSQLException {
try {
final List<String> advertisedMechanisms = advertisedMechanisms(stream);
final byte[] cbindData = extractChannelBindingData(stream);

ScramClient client = ScramClient.builder()
.advertisedMechanisms(advertisedMechanisms)
.username("*") // username is ignored by server, startup message is used instead
.password(password)
.channelBinding(TlsServerEndpoint.TLS_SERVER_END_POINT, cbindData)
.stringPreparation(StringPreparation.POSTGRESQL_PREPARATION)
.build();

if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.log(Level.FINEST, " Using SCRAM mechanism: {0}",
client.getScramMechanism().getName());
}
return client;
} catch (IllegalArgumentException | IOException e) {
throw new PSQLException(
GT.tr("Invalid SCRAM client initialization", e),
PSQLState.CONNECTION_REJECTED);
}
}

private static List<String> advertisedMechanisms(PGStream stream) throws PSQLException, IOException {
List<String> mechanisms = new ArrayList<>();
do {
mechanisms.add(stream.receiveString());
} while (stream.peekChar() != 0);
int c = stream.receiveChar();
assert c == 0;
if (mechanisms.isEmpty()) {
throw new PSQLException(
GT.tr("Received AuthenticationSASL message with 0 mechanisms!"),
PSQLState.CONNECTION_REJECTED);
}
LOGGER.log(Level.FINEST, " <=BE AuthenticationSASL( {0} )", mechanisms);
return mechanisms;
}

private static byte[] extractChannelBindingData(PGStream stream) {
Socket socket = stream.getSocket();
if (socket instanceof SSLSocket) {
SSLSession session = ((SSLSocket) socket).getSession();
try {
Certificate[] certificates = session.getPeerCertificates();
if (certificates != null && certificates.length > 0) {
Certificate peerCert = certificates[0]; // First certificate is the peer's certificate
if (peerCert instanceof X509Certificate) {
X509Certificate cert = (X509Certificate) peerCert;
return TlsServerEndpoint.getChannelBindingData(cert);
}
}
} catch (CertificateEncodingException | SSLException e) {
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.log(Level.FINEST, "Error extracting channel binding data", e);
}
}
}
return new byte[0];
}

void handleAuthenticationSASL() throws IOException {
ClientFirstMessage clientFirstMessage = scramClient.clientFirstMessage();
LOGGER.log(Level.FINEST, " FE=> SASLInitialResponse( {0} )", clientFirstMessage);
String scramMechanismName = scramClient.getScramMechanism().getName();
final byte[] scramMechanismNameBytes = scramMechanismName.getBytes(StandardCharsets.UTF_8);
final byte[] clientFirstMessageBytes =
clientFirstMessage.toString().getBytes(StandardCharsets.UTF_8);
sendAuthenticationMessage(
(scramMechanismNameBytes.length + 1) + 4 + clientFirstMessageBytes.length,
pgStream -> {
pgStream.send(scramMechanismNameBytes);
pgStream.sendChar(0); // List terminated in '\0'
pgStream.sendInteger4(clientFirstMessageBytes.length);
pgStream.send(clientFirstMessageBytes);
});
}

void handleAuthenticationSASLContinue(int length) throws IOException, PSQLException {
String receivedServerFirstMessage = pgStream.receiveString(length);
LOGGER.log(Level.FINEST, " <=BE AuthenticationSASLContinue( {0} )", receivedServerFirstMessage);
try {
scramClient.serverFirstMessage(receivedServerFirstMessage);
} catch (ScramException | IllegalStateException | IllegalArgumentException e) {
throw new PSQLException(
GT.tr("SCRAM authentication failed: {0}", e.getMessage()),
PSQLState.CONNECTION_REJECTED,
e);
}

ClientFinalMessage clientFinalMessage = scramClient.clientFinalMessage();
LOGGER.log(Level.FINEST, " FE=> SASLResponse( {0} )", clientFinalMessage);
final byte[] clientFinalMessageBytes =
clientFinalMessage.toString().getBytes(StandardCharsets.UTF_8);
sendAuthenticationMessage(
clientFinalMessageBytes.length,
pgStream -> pgStream.send(clientFinalMessageBytes)
);
}

void handleAuthenticationSASLFinal(int length) throws IOException, PSQLException {
String serverFinalMessage = pgStream.receiveString(length);
LOGGER.log(Level.FINEST, " <=BE AuthenticationSASLFinal( {0} )", serverFinalMessage);
try {
scramClient.serverFinalMessage(serverFinalMessage);
} catch (ScramException | IllegalStateException | IllegalArgumentException e) {
throw new PSQLException(
GT.tr("SCRAM authentication failed: {0}", e.getMessage()),
PSQLState.CONNECTION_REJECTED,
e);
}
}

private interface BodySender {
void sendBody(PGStream pgStream) throws IOException;
}

private void sendAuthenticationMessage(int bodyLength, BodySender bodySender)
throws IOException {
pgStream.sendChar('p');
pgStream.sendInteger4(Integer.BYTES + bodyLength);
bodySender.sendBody(pgStream);
pgStream.flush();
}
}

0 comments on commit 84e538b

Please sign in to comment.