Skip to content

Commit

Permalink
Gssapi encrypted (#1821)
Browse files Browse the repository at this point in the history
* add test-gss to test gss encrypted mode

* chore: add gss encrypted mode
This was added in PostgreSQL 12
After creating an initial socket we then try to elevate the connection to GSS encrypted
If this fails we then try ssl
All of this is dependent on the gssEncMode 

* try normal authentication

* use encrypted authentication when necessary

* make test fail using Assert.that if they do not succeed

* document use of gssEncMode, add entry to changelog

Co-authored-by: Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
  • Loading branch information
davecramer and vlsi committed Jul 18, 2020
1 parent 44b1247 commit ad921b9
Show file tree
Hide file tree
Showing 29 changed files with 1,433 additions and 25 deletions.
45 changes: 45 additions & 0 deletions .github/workflows/testgss.yml
@@ -0,0 +1,45 @@
# This is a basic workflow to help you get started with Actions

name: CI

# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
push:
branches:
- '*'
pull_request:
branches:
- '*'

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
gss-encryption:
# The type of runner that the job will run on
runs-on: ubuntu-latest

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/checkout@v2

- name: 'Set up JDK 8'
uses: actions/setup-java@v1
with:
java-version: 8

- name: 'install software'
run: sudo apt -y install krb5-kdc krb5-admin-server libkrb5-dev postgresql-12

- name: 'update hosts'
run: sudo -- sh -c "echo 127.0.0.1 auth-test-localhost.postgresql.example.com >> /etc/hosts"

- name: 'build branch'
run: ./gradlew publishToMavenLocal -Ppgjdbc.version=1.0.0-dev-master -PskipJavadoc

# Runs a set of commands using the runners shell
- name: Run a multi-line script
run: |
cd test-gss
./gradlew assemble
./gradlew run
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Changed
- Rename source distribution archive to `postgresql-$version-jdbc-src.tar.gz`, and add top-level archive folder
- add the ability to connect with a GSSAPI encrypted connection. As of PostgreSQL version 12 GSSAPI encrypted connections
are possible. Now the driver will attempt to connect to the server with a GSSAPI encrypted connection. If that fails then
attempt an SSL connection, finally falling back to a plain text connection. All of this is controlled using both the gssEncMode
and sslMode parameters which, in concert with pg_hba.conf, determine if a particular mode is allowed and or required.

## [42.2.15] (2020-06-19)
### Changed
Expand Down
7 changes: 4 additions & 3 deletions README.md
Expand Up @@ -114,7 +114,7 @@ In addition to the standard connection parameters the driver supports a number o
| ssl | Boolean | false | Control use of SSL (true value causes SSL to be required) |
| sslfactory | String | null | Provide a SSLSocketFactory class when using SSL. |
| sslfactoryarg (deprecated) | String | null | Argument forwarded to constructor of SSLSocketFactory class. |
| sslmode | String | null | Parameter governing the use of SSL. |
| sslmode | String | prefer | Controls the preference for opening using an SSL encrypted connection. |
| sslcert | String | null | The location of the client's SSL certificate |
| sslkey | String | null | The location of the client's PKCS#8 SSL key |
| sslrootcert | String | null | The location of the root certificate for authenticating the server. |
Expand Down Expand Up @@ -150,9 +150,10 @@ In addition to the standard connection parameters the driver supports a number o
| autosave | String | never | Specifies what the driver should do if a query fails, possible values: always, never, conservative |
| cleanupSavepoints | Boolean | false | In Autosave mode the driver sets a SAVEPOINT for every query. It is possible to exhaust the server shared buffers. Setting this to true will release each SAVEPOINT at the cost of an additional round trip. |
| preferQueryMode | String | extended | Specifies which mode is used to execute queries to database, possible values: extended, extendedForPrepared, extendedCacheEverything, simple |
| reWriteBatchedInserts | Boolean | false | Enable optimization to rewrite and collapse compatible INSERT statements that are batched. |
| escapeSyntaxCallMode | String | select | Specifies how JDBC escape call syntax is transformed into underlying SQL (CALL/SELECT), for invoking procedures or functions (requires server version >= 11), possible values: select, callIfNoReturn, call |
| reWriteBatchedInserts | Boolean | false | Enable optimization to rewrite and collapse compatible INSERT statements that are batched. |
| escapeSyntaxCallMode | String | select | Specifies how JDBC escape call syntax is transformed into underlying SQL (CALL/SELECT), for invoking procedures or functions (requires server version >= 11), possible values: select, callIfNoReturn, call |
| maxResultBuffer | String | null | Specifies size of result buffer in bytes, which can't be exceeded during reading result set. Can be specified as particular size (i.e. "100", "200M" "2G") or as percent of max heap memory (i.e. "10p", "20pct", "50percent") |
| gssEncMode | String | prefer | Controls the preference for using GSSAPI encryption for the connection, values are disable, allow, prefer, and require |

## Contributing
For information on how to contribute to the project see the [Contributing Guidelines](CONTRIBUTING.md)
Expand Down
1 change: 1 addition & 0 deletions build.properties
Expand Up @@ -14,6 +14,7 @@ username=test
password=test
privilegedUser=postgres
privilegedPassword=
gssEncMode=disable
sspiusername=testsspi
preparethreshold=5
loggerLevel=OFF
Expand Down
20 changes: 15 additions & 5 deletions docs/documentation/head/connect.md
Expand Up @@ -113,7 +113,7 @@ Connection conn = DriverManager.getConnection(url);
. `require`, `allow` and `prefer` all default to a non validating SSL factory and do not check the
validity of the certificate or the host name. `verify-ca` validates the certificate, but does not
verify the hostname. `verify-full` will validate that the certificate is correct and verify the
host connected to has the same hostname as the certificate.
host connected to has the same hostname as the certificate. Default is `prefer`

Setting these will necessitate storing the server certificate on the client machine see
["Configuring the client"](ssl-client.html) for details.
Expand Down Expand Up @@ -352,6 +352,12 @@ Connection conn = DriverManager.getConnection(url);
you are unable to change the application to use an appropriate method
such as `setInt()`.

* **ApplicationName** = String

Specifies the name of the application that is using the connection.
This allows a database administrator to see what applications are
connected to the server and what resources they are using through views like pg_stat_activity.

* **kerberosServerName** = String

The Kerberos service name to use when authenticating with GSSAPI. This
Expand All @@ -370,11 +376,15 @@ Connection conn = DriverManager.getConnection(url);
authenticating. To skip the JAAS login, for example if the native GSS
implementation is being used to obtain credentials, set this to `false`.

* **ApplicationName** = String
* **gssEncMode** = String

Specifies the name of the application that is using the connection.
This allows a database administrator to see what applications are
connected to the server and what resources they are using through views like pg_stat_activity.
PostgreSQL 12 and later now allow GSSAPI encrypted connections. This parameter controls whether to
enforce using GSSAPI encryption or not. The options are `disable`, `allow`, `prefer` and `require`
`disable` is obvious and disables any attempt to connect using GSS encrypted mode
`allow` will connect in plain text then if the server requests it will switch to encrypted mode
`prefer` will attempt connect in encrypted mode and fall back to plain text if it fails to acquire
an encrypted connection
`require` attempts to connect in encrypted mode and will fail to connect if that is not possible.

* **gsslib** = String

Expand Down
8 changes: 8 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/PGProperty.java
Expand Up @@ -180,6 +180,14 @@ public enum PGProperty {
false,
new String[] {"select", "callIfNoReturn", "call"}),

GSS_ENC_MODE(
"gssEncMode",
"prefer",
"Force Encoded GSS Mode",
false,
new String[] {"disable", "prefer", "require"}
),

/**
* Force one of
* <ul>
Expand Down
20 changes: 19 additions & 1 deletion pgjdbc/src/main/java/org/postgresql/core/PGStream.java
Expand Up @@ -5,13 +5,18 @@

package org.postgresql.core;

import org.postgresql.gss.GSSInputStream;
import org.postgresql.gss.GSSOutputStream;
import org.postgresql.util.ByteStreamWriter;
import org.postgresql.util.GT;
import org.postgresql.util.HostSpec;
import org.postgresql.util.PGPropertyMaxResultBufferParser;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;

import org.ietf.jgss.GSSContext;
import org.ietf.jgss.MessageProp;

import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.EOFException;
Expand Down Expand Up @@ -48,6 +53,20 @@ public class PGStream implements Closeable, Flushable {
private OutputStream pgOutput;
private byte[] streamBuffer;

public boolean isGssEncrypted() {
return gssEncrypted;
}

boolean gssEncrypted = false;

public void setSecContext(GSSContext secContext) {
MessageProp messageProp = new MessageProp(0, true);
pgInput = new VisibleBufferedInputStream(new GSSInputStream(pgInput.getWrapped(), secContext, messageProp ), 8192);
pgOutput = new GSSOutputStream(pgOutput, secContext, messageProp, 16384);
gssEncrypted = true;

}

private long nextStreamAvailableCheckTime;
// This is a workaround for SSL sockets: sslInputStream.available() might return 0
// so we perform "1ms reads" once in a while
Expand Down Expand Up @@ -324,7 +343,6 @@ public void sendInteger2(int val) throws IOException {
if (val < Short.MIN_VALUE || val > Short.MAX_VALUE) {
throw new IOException("Tried to send an out-of-range integer as a 2-byte value: " + val);
}

int2Buf[0] = (byte) (val >>> 8);
int2Buf[1] = (byte) val;
pgOutput.write(int2Buf);
Expand Down
Expand Up @@ -337,4 +337,12 @@ public int scanCStringLength() throws IOException {
public void setTimeoutRequested(boolean timeoutRequested) {
this.timeoutRequested = timeoutRequested;
}

/**
*
* @return the wrapped stream
*/
public InputStream getWrapped() {
return wrapped;
}
}
Expand Up @@ -22,6 +22,7 @@
import org.postgresql.hostchooser.HostChooserFactory;
import org.postgresql.hostchooser.HostRequirement;
import org.postgresql.hostchooser.HostStatus;
import org.postgresql.jdbc.GSSEncMode;
import org.postgresql.jdbc.SslMode;
import org.postgresql.sspi.ISSPIClient;
import org.postgresql.util.GT;
Expand Down Expand Up @@ -86,7 +87,7 @@ private ISSPIClient createSSPI(PGStream pgStream,

private PGStream tryConnect(String user, String database,
Properties info, SocketFactory socketFactory, HostSpec hostSpec,
SslMode sslMode)
SslMode sslMode, GSSEncMode gssEncMode)
throws SQLException, IOException {
int connectTimeout = PGProperty.CONNECT_TIMEOUT.getInt(info) * 1000;

Expand Down Expand Up @@ -136,9 +137,13 @@ private PGStream tryConnect(String user, String database,
LOGGER.log(Level.FINE, "Send Buffer Size is {0}", newStream.getSocket().getSendBufferSize());
}

// Construct and send an ssl startup packet if requested.
newStream = enableSSL(newStream, sslMode, info, connectTimeout);
newStream = enableGSSEncrypted(newStream, gssEncMode, hostSpec.getHost(), user, info, connectTimeout);

// if we have a security context then gss negotiation succeeded. Do not attempt SSL negotiation
if (!newStream.isGssEncrypted()) {
// Construct and send an ssl startup packet if requested.
newStream = enableSSL(newStream, sslMode, info, connectTimeout);
}
List<String[]> paramList = getParametersForStartup(user, database, info);
sendStartupPacket(newStream, paramList);

Expand All @@ -152,6 +157,7 @@ private PGStream tryConnect(String user, String database,
public QueryExecutor openConnectionImpl(HostSpec[] hostSpecs, String user, String database,
Properties info) throws SQLException {
SslMode sslMode = SslMode.of(info);
GSSEncMode gssEncMode = GSSEncMode.of(info);

HostRequirement targetServerType;
String targetServerTypeStr = PGProperty.TARGET_SERVER_TYPE.get(info);
Expand Down Expand Up @@ -194,7 +200,7 @@ public QueryExecutor openConnectionImpl(HostSpec[] hostSpecs, String user, Strin
PGStream newStream = null;
try {
try {
newStream = tryConnect(user, database, info, socketFactory, hostSpec, sslMode);
newStream = tryConnect(user, database, info, socketFactory, hostSpec, sslMode, gssEncMode);
} catch (SQLException e) {
if (sslMode == SslMode.PREFER
&& PSQLState.INVALID_AUTHORIZATION_SPECIFICATION.getState().equals(e.getSQLState())) {
Expand All @@ -203,7 +209,7 @@ public QueryExecutor openConnectionImpl(HostSpec[] hostSpecs, String user, Strin
Throwable ex = null;
try {
newStream =
tryConnect(user, database, info, socketFactory, hostSpec, SslMode.DISABLE);
tryConnect(user, database, info, socketFactory, hostSpec, SslMode.DISABLE,gssEncMode);
LOGGER.log(Level.FINE, "Downgraded to non-encrypted connection for host {0}",
hostSpec);
} catch (SQLException ee) {
Expand All @@ -226,7 +232,7 @@ public QueryExecutor openConnectionImpl(HostSpec[] hostSpecs, String user, Strin
Throwable ex = null;
try {
newStream =
tryConnect(user, database, info, socketFactory, hostSpec, SslMode.REQUIRE);
tryConnect(user, database, info, socketFactory, hostSpec, SslMode.REQUIRE, gssEncMode);
LOGGER.log(Level.FINE, "Upgraded to encrypted connection for host {0}",
hostSpec);
} catch (SQLException ee) {
Expand Down Expand Up @@ -393,6 +399,77 @@ private static String createPostgresTimeZone() {
return start + tz.substring(4);
}

private PGStream enableGSSEncrypted(PGStream pgStream, GSSEncMode gssEncMode, String host, String user, Properties info,
int connectTimeout)
throws IOException, PSQLException {

if ( gssEncMode == GSSEncMode.DISABLE ) {
return pgStream;
}

if (gssEncMode == GSSEncMode.ALLOW ) {
// start with plain text and let the server request it
return pgStream;
}

String password = PGProperty.PASSWORD.get(info);
LOGGER.log(Level.FINEST, " FE=> GSSENCRequest");

// Send SSL request packet
pgStream.sendInteger4(8);
pgStream.sendInteger2(1234);
pgStream.sendInteger2(5680);
pgStream.flush();
// Now get the response from the backend, one of N, E, S.
int beresp = pgStream.receiveChar();
switch (beresp) {
case 'E':
LOGGER.log(Level.FINEST, " <=BE GSSEncrypted Error");

// Server doesn't even know about the SSL handshake protocol
if (gssEncMode.requireEncryption()) {
throw new PSQLException(GT.tr("The server does not support GSS Encoding."),
PSQLState.CONNECTION_REJECTED);
}

// We have to reconnect to continue.
pgStream.close();
return new PGStream(pgStream.getSocketFactory(), pgStream.getHostSpec(), connectTimeout);

case 'N':
LOGGER.log(Level.FINEST, " <=BE GSSEncrypted Refused");

// Server does not support gss encryption
if (gssEncMode.requireEncryption()) {
throw new PSQLException(GT.tr("The server does not support GSS Encryption."),
PSQLState.CONNECTION_REJECTED);
}

return pgStream;

case 'G':
LOGGER.log(Level.FINEST, " <=BE GSSEncryptedOk");
try {
org.postgresql.gss.MakeGSS.authenticate(true, pgStream, host, user, password,
PGProperty.JAAS_APPLICATION_NAME.get(info),
PGProperty.KERBEROS_SERVER_NAME.get(info), false, // TODO: fix this
PGProperty.JAAS_LOGIN.getBoolean(info),
PGProperty.LOG_SERVER_ERROR_DETAIL.getBoolean(info));
return pgStream;
} catch (PSQLException ex) {
// allow the connection to proceed
if ( gssEncMode == GSSEncMode.PREFER) {
// we have to reconnect to continue
return new PGStream(pgStream, connectTimeout);
}
}

default:
throw new PSQLException(GT.tr("An error occurred while setting up the GSS Encoded connection."),
PSQLState.PROTOCOL_VIOLATION);
}
}

private PGStream enableSSL(PGStream pgStream, SslMode sslMode, Properties info,
int connectTimeout)
throws IOException, PSQLException {
Expand Down Expand Up @@ -648,7 +725,7 @@ private void doAuthentication(PGStream pgStream, String host, String user, Prope
sspiClient.startSSPI();
} else {
/* Use JGSS's GSSAPI for this request */
org.postgresql.gss.MakeGSS.authenticate(pgStream, host, user, password,
org.postgresql.gss.MakeGSS.authenticate(false, pgStream, host, user, password,
PGProperty.JAAS_APPLICATION_NAME.get(info),
PGProperty.KERBEROS_SERVER_NAME.get(info), usespnego,
PGProperty.JAAS_LOGIN.getBoolean(info),
Expand Down
16 changes: 16 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
Expand Up @@ -1056,6 +1056,22 @@ public void setGssLib(String lib) {
PGProperty.GSS_LIB.set(properties, lib);
}

/**
*
* @return GSS encryption mode: disable, prefer or require
*/
public String getGssEncMode() {
return PGProperty.GSS_ENC_MODE.get(properties);
}

/**
*
* @param mode encryption mode: disable, prefer or require
*/
public void setGssEncMode(String mode) {
PGProperty.GSS_ENC_MODE.set(properties, mode);
}

/**
* @return SSPI service class
* @see PGProperty#SSPI_SERVICE_CLASS
Expand Down

0 comments on commit ad921b9

Please sign in to comment.