Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update SCRAM dependency to 3.0 and support channel binding #3188

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 8 additions & 4 deletions pgjdbc/build.gradle.kts
Original file line number Diff line number Diff line change
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"
}
Comment on lines +191 to 196
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If scram-* bundles the license file, we could probably remove customizations here, and remove /licenses/* files.
WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it bundle the license now, not sure what exact does Gradle here, so if it's not needed to do anything else here, it could be probably removed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The configuration here sets to use license files from https://github.com/pgjdbc/pgjdbc/tree/master/licenses/scram for the scram library. Ideally, libraries should bundle their licenses (and the licenses of the shaded libraries as well), so in the ideal world, overrides should not be needed. However, previous scram versions missed the license files so we had to keep them in pgjdbc source tree

Copy link
Member Author

@jorsol jorsol Apr 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ouch!, I just checked that the stringprep dependency does not have bundled the LICENSE file, was completely an oversight, so we might need to keep the overrides a bit longer.

I will need to add a test to ensure this never happens in the future.

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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();
}
}