From 84e538b05693c57953bcdbdb9aa4fcebb6d2184f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sol=C3=B3rzano?= Date: Sun, 7 Apr 2024 17:00:00 +0000 Subject: [PATCH] Update SCRAM dependency to 3.0 and support channel binding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jorge Solórzano --- pgjdbc/build.gradle.kts | 12 +- pgjdbc/reduced-pom.xml | 4 +- .../core/v3/ConnectionFactoryImpl.java | 20 +- .../core/v3/ScramAuthenticator.java | 172 +++++++++++++++ .../jre7/sasl/ScramAuthenticator.java | 198 ------------------ .../org/postgresql/util/PasswordUtil.java | 31 ++- 6 files changed, 201 insertions(+), 236 deletions(-) create mode 100644 pgjdbc/src/main/java/org/postgresql/core/v3/ScramAuthenticator.java delete mode 100644 pgjdbc/src/main/java/org/postgresql/jre7/sasl/ScramAuthenticator.java diff --git a/pgjdbc/build.gradle.kts b/pgjdbc/build.gradle.kts index f1820edaf7..1ddd2ad989 100644 --- a/pgjdbc/build.gradle.kts +++ b/pgjdbc/build.gradle.kts @@ -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") @@ -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") { @@ -224,6 +224,10 @@ tasks.configureEach { 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") { @@ -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 """ diff --git a/pgjdbc/reduced-pom.xml b/pgjdbc/reduced-pom.xml index 119a38b6f2..8890355af2 100644 --- a/pgjdbc/reduced-pom.xml +++ b/pgjdbc/reduced-pom.xml @@ -43,8 +43,8 @@ com.ongres.scram - client - %{com.ongres.scram:client} + scram-client + %{com.ongres.scram:scram-client} se.jiderhamn diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java index bc0af8e4cc..605c720704 100644 --- a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java +++ b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java @@ -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; @@ -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( @@ -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: diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/ScramAuthenticator.java b/pgjdbc/src/main/java/org/postgresql/core/v3/ScramAuthenticator.java new file mode 100644 index 0000000000..8fcde26599 --- /dev/null +++ b/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 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 advertisedMechanisms(PGStream stream) throws PSQLException, IOException { + List 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(); + } +} diff --git a/pgjdbc/src/main/java/org/postgresql/jre7/sasl/ScramAuthenticator.java b/pgjdbc/src/main/java/org/postgresql/jre7/sasl/ScramAuthenticator.java deleted file mode 100644 index ff96106a35..0000000000 --- a/pgjdbc/src/main/java/org/postgresql/jre7/sasl/ScramAuthenticator.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (c) 2017, PostgreSQL Global Development Group - * See the LICENSE file in the project root for more information. - */ - -package org.postgresql.jre7.sasl; - -import static org.postgresql.util.internal.Nullness.castNonNull; - -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.client.ScramSession; -import com.ongres.scram.common.exception.ScramException; -import com.ongres.scram.common.exception.ScramInvalidServerSignatureException; -import com.ongres.scram.common.exception.ScramParseException; -import com.ongres.scram.common.exception.ScramServerErrorException; -import com.ongres.scram.common.stringprep.StringPreparations; -import org.checkerframework.checker.nullness.qual.Nullable; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class ScramAuthenticator { - private static final Logger LOGGER = Logger.getLogger(ScramAuthenticator.class.getName()); - - private final String user; - private final String password; - private final PGStream pgStream; - private @Nullable ScramClient scramClient; - private @Nullable ScramSession scramSession; - private ScramSession.@Nullable ClientFinalProcessor clientFinalProcessor; - - private interface BodySender { - void sendBody(PGStream pgStream) throws IOException; - } - - private void sendAuthenticationMessage(int bodyLength, BodySender bodySender) - throws IOException { - pgStream.sendChar('p'); - pgStream.sendInteger4(Integer.SIZE / Byte.SIZE + bodyLength); - bodySender.sendBody(pgStream); - pgStream.flush(); - } - - public ScramAuthenticator(String user, String password, PGStream pgStream) { - this.user = user; - this.password = password; - this.pgStream = pgStream; - } - - public void processServerMechanismsAndInit() throws IOException, PSQLException { - List mechanisms = new ArrayList<>(); - do { - mechanisms.add(pgStream.receiveString()); - } while (pgStream.peekChar() != 0); - int c = pgStream.receiveChar(); - assert c == 0; - if (mechanisms.isEmpty()) { - throw new PSQLException( - GT.tr("No SCRAM mechanism(s) advertised by the server"), - PSQLState.CONNECTION_REJECTED - ); - } - - ScramClient scramClient; - try { - scramClient = ScramClient - .channelBinding(ScramClient.ChannelBinding.NO) - .stringPreparation(StringPreparations.SASL_PREPARATION) - .selectMechanismBasedOnServerAdvertised(mechanisms.toArray(new String[]{})) - .setup(); - } catch (IllegalArgumentException e) { - throw new PSQLException( - GT.tr("Invalid or unsupported by client SCRAM mechanisms", e), - PSQLState.CONNECTION_REJECTED - ); - } - if (LOGGER.isLoggable(Level.FINEST)) { - LOGGER.log(Level.FINEST, " Using SCRAM mechanism {0}", scramClient.getScramMechanism().getName()); - } - - this.scramClient = scramClient; - scramSession = - scramClient.scramSession("*"); // Real username is ignored by server, uses startup one - } - - public void sendScramClientFirstMessage() throws IOException { - ScramSession scramSession = this.scramSession; - String clientFirstMessage = castNonNull(scramSession).clientFirstMessage(); - LOGGER.log(Level.FINEST, " FE=> SASLInitialResponse( {0} )", clientFirstMessage); - - ScramClient scramClient = this.scramClient; - String scramMechanismName = castNonNull(scramClient).getScramMechanism().getName(); - final byte[] scramMechanismNameBytes = scramMechanismName.getBytes(StandardCharsets.UTF_8); - final byte[] clientFirstMessageBytes = clientFirstMessage.getBytes(StandardCharsets.UTF_8); - sendAuthenticationMessage( - (scramMechanismNameBytes.length + 1) + 4 + clientFirstMessageBytes.length, - new BodySender() { - @Override - public void sendBody(PGStream pgStream) throws IOException { - pgStream.send(scramMechanismNameBytes); - pgStream.sendChar(0); // List terminated in '\0' - pgStream.sendInteger4(clientFirstMessageBytes.length); - pgStream.send(clientFirstMessageBytes); - } - } - ); - } - - public void processServerFirstMessage(int length) throws IOException, PSQLException { - String serverFirstMessage = pgStream.receiveString(length); - LOGGER.log(Level.FINEST, " <=BE AuthenticationSASLContinue( {0} )", serverFirstMessage); - - ScramSession scramSession = this.scramSession; - if (scramSession == null) { - throw new PSQLException( - GT.tr("SCRAM session does not exist"), - PSQLState.UNKNOWN_STATE - ); - } - - ScramSession.ServerFirstProcessor serverFirstProcessor; - try { - serverFirstProcessor = scramSession.receiveServerFirstMessage(serverFirstMessage); - } catch (ScramException e) { - throw new PSQLException( - GT.tr("Invalid server-first-message: {0}", serverFirstMessage), - PSQLState.CONNECTION_REJECTED, - e - ); - } - if (LOGGER.isLoggable(Level.FINEST)) { - LOGGER.log(Level.FINEST, - " <=BE AuthenticationSASLContinue(salt={0}, iterations={1})", - new Object[]{serverFirstProcessor.getSalt(), serverFirstProcessor.getIteration()} - ); - } - - clientFinalProcessor = serverFirstProcessor.clientFinalProcessor(password); - - String clientFinalMessage = clientFinalProcessor.clientFinalMessage(); - LOGGER.log(Level.FINEST, " FE=> SASLResponse( {0} )", clientFinalMessage); - - final byte[] clientFinalMessageBytes = clientFinalMessage.getBytes(StandardCharsets.UTF_8); - sendAuthenticationMessage( - clientFinalMessageBytes.length, - new BodySender() { - @Override - public void sendBody(PGStream pgStream) throws IOException { - pgStream.send(clientFinalMessageBytes); - } - } - ); - } - - public void verifyServerSignature(int length) throws IOException, PSQLException { - String serverFinalMessage = pgStream.receiveString(length); - LOGGER.log(Level.FINEST, " <=BE AuthenticationSASLFinal( {0} )", serverFinalMessage); - - ScramSession.ClientFinalProcessor clientFinalProcessor = this.clientFinalProcessor; - if (clientFinalProcessor == null) { - throw new PSQLException( - GT.tr("SCRAM client final processor does not exist"), - PSQLState.UNKNOWN_STATE - ); - } - try { - clientFinalProcessor.receiveServerFinalMessage(serverFinalMessage); - } catch (ScramParseException e) { - throw new PSQLException( - GT.tr("Invalid server-final-message: {0}", serverFinalMessage), - PSQLState.CONNECTION_REJECTED, - e - ); - } catch (ScramServerErrorException e) { - throw new PSQLException( - GT.tr("SCRAM authentication failed, server returned error: {0}", - e.getError().getErrorMessage()), - PSQLState.CONNECTION_REJECTED, - e - ); - } catch (ScramInvalidServerSignatureException e) { - throw new PSQLException( - GT.tr("Invalid server SCRAM signature"), - PSQLState.CONNECTION_REJECTED, - e - ); - } - } -} diff --git a/pgjdbc/src/main/java/org/postgresql/util/PasswordUtil.java b/pgjdbc/src/main/java/org/postgresql/util/PasswordUtil.java index 624c11461d..bbd5f16e45 100644 --- a/pgjdbc/src/main/java/org/postgresql/util/PasswordUtil.java +++ b/pgjdbc/src/main/java/org/postgresql/util/PasswordUtil.java @@ -8,9 +8,8 @@ import org.postgresql.core.Utils; import com.ongres.scram.common.ScramFunctions; -import com.ongres.scram.common.ScramMechanisms; -import com.ongres.scram.common.bouncycastle.base64.Base64; -import com.ongres.scram.common.stringprep.StringPreparations; +import com.ongres.scram.common.ScramMechanism; +import com.ongres.scram.common.StringPreparation; import java.nio.ByteBuffer; import java.nio.CharBuffer; @@ -20,6 +19,7 @@ import java.security.SecureRandom; import java.sql.SQLException; import java.util.Arrays; +import java.util.Base64; import java.util.Objects; public class PasswordUtil { @@ -58,18 +58,18 @@ public static String encodeScramSha256(char[] password, int iterations, byte[] s throw new IllegalArgumentException("salt length must be greater than zero"); } try { - String passwordText = String.valueOf(password); - byte[] saltedPassword = ScramFunctions.saltedPassword(ScramMechanisms.SCRAM_SHA_256, - StringPreparations.SASL_PREPARATION, passwordText, salt, iterations); - byte[] clientKey = ScramFunctions.clientKey(ScramMechanisms.SCRAM_SHA_256, saltedPassword); - byte[] storedKey = ScramFunctions.storedKey(ScramMechanisms.SCRAM_SHA_256, clientKey); - byte[] serverKey = ScramFunctions.serverKey(ScramMechanisms.SCRAM_SHA_256, saltedPassword); - - return "SCRAM-SHA-256" + ScramMechanism scramSha256 = ScramMechanism.SCRAM_SHA_256; + byte[] saltedPassword = ScramFunctions.saltedPassword(scramSha256, + StringPreparation.POSTGRESQL_PREPARATION, password, salt, iterations); + byte[] clientKey = ScramFunctions.clientKey(scramSha256, saltedPassword); + byte[] storedKey = ScramFunctions.storedKey(scramSha256, clientKey); + byte[] serverKey = ScramFunctions.serverKey(scramSha256, saltedPassword); + + return scramSha256.getName() + "$" + iterations - + ":" + Base64.toBase64String(salt) - + "$" + Base64.toBase64String(storedKey) - + ":" + Base64.toBase64String(serverKey); + + ":" + Base64.getEncoder().encodeToString(salt) + + "$" + Base64.getEncoder().encodeToString(storedKey) + + ":" + Base64.getEncoder().encodeToString(serverKey); } finally { Arrays.fill(password, (char) 0); } @@ -87,8 +87,7 @@ public static String encodeScramSha256(char[] password, int iterations, byte[] s public static String encodeScramSha256(char[] password) { Objects.requireNonNull(password, "password"); try { - SecureRandom rng = getSecureRandom(); - byte[] salt = rng.generateSeed(DEFAULT_SALT_LENGTH); + byte[] salt = ScramFunctions.salt(DEFAULT_SALT_LENGTH, getSecureRandom()); return encodeScramSha256(password, DEFAULT_ITERATIONS, salt); } finally { Arrays.fill(password, (char) 0);