diff -Nur postgresql-jdbc-8.3-604.src.orig/doc/pgjdbc.xml postgresql-jdbc-8.3-604.src/doc/pgjdbc.xml --- postgresql-jdbc-8.3-604.src.orig/doc/pgjdbc.xml 2008-01-08 07:56:26.000000000 +0100 +++ postgresql-jdbc-8.3-604.src/doc/pgjdbc.xml 2009-07-01 09:54:45.029843000 +0200 @@ -450,6 +450,17 @@ + krbsrvname = String + + + Use the given string as Kerberos service name instead of + postgres. For more information see . + + + + + compatible = String @@ -796,6 +807,247 @@ + + Using <acronym>GSS</acronym> + + + The Generic Security Services Application Program Interface + (GSS API or + GSSAPI) enables the JDBC driver to + authenticate the user via Kerberos. + + + + Kerberos makes a single sign on solution possible. It is a very secure + authentication method and needs only a few changes to the system. The + JDBC driver supports Linux- and Windows-clients and + possibly other systems which have GSS + API support in Java. + + + + + PostgreSQL supports different Kerberos + authentication types (Kerberos 4, Kerberos 5, GSS API + and SSPI). This chapter describes only Kerberos 5 with + the help of the GSS API. + + + + + Server Configuration + + + Please read the documentation of the server for further details on + configuring the PostgreSQL server for + Kerberos/GSSAPI. A very, very short description: + + + + You have to create a service principal + (postgres/fqdn@REALM), extract its key to a keytab which + is readable by the user running the server. Some variables should be set in + the configuration file: + + + + krb_server_keyfile = '/path/to/keytab' + krb_server_hostname = 'fqdn-of-server' + + + + Normally you should not need to define these values. But on a multihomed + host it is better to define the hostname. The path to the keytab is + neccessary because normally you would not use the system's keytab (e.g. + /etc/krb5.keytab) which should only be readable by the + superuser. + + + + In the pg_hba.conf you should enable + gss: + + + + # TYPE DATABASE USER CIDR-ADDRESS METHOD + host all all 10.0.0.0/8 gss + + + + + Client Configuration + + + You need to configure Kerberos on the client prior using the + JDBC driver. This section describes the client + configuration on Linux and Windows. + + + + Linux + + + The system has to be configured for Kerberos. Some distributions + integrate Kerberos very well, so there is nothing to do except of using + it. To configure it manually you need to setup the Kerberos + configuration file /etc/krb5.conf: + + + + [libdefaults] + default_realm = EXAMPLE.ORG + + [realms] + EXAMPLE.ORG = { + kdc = kerberos.example.org + admin_server = kerberos.example.org + } + + [domain_realm] + .example.org = EXAMPLE.ORG + + + + This is a minimal krb5.conf. It contains the default + Kerberos realm (EXAMPLE.ORG) and which key + destribution center (KDC) and administration server + (AS) is responsible for this realm. For further + information you should take a look in the documentation of the Kerberos + implementation. + + + + Afterwards you have to initialize the Kerberos environment by executing + kinit. With klist you can check, if everything is fine. It should + output something like this: + + + + cju@box:~/> klist + Ticket cache: FILE:/tmp/krb5cc_1003 + Default principal: cju@EXAMPLE.ORG + + Valid starting Expires Service principal + 06/26/09 14:07:15 06/27/09 14:07:15 krbtgt/EXAMPLE.ORG@EXAMPLE.ORG + + + Kerberos 4 ticket cache: /tmp/tkt1003 + klist: You have no tickets cached + + + + klist should show you a valid Ticket Granting Ticket + (TGT). You can now configure the + JDBC driver and use it. + + + + + Windows + + + Windows 2000 Professional and Server, Windows XP and Windows 2003 + Server support natively Kerberos as long as they are member of either a + Kerberos Realm or an Active Directory Domain. + + + + Some changes are neccessary to use third-party software and especially + Java with Kerberos. To understand these changes, a descent knowledge of + the native Microsoft Kerberos implementation is useful: + + + + Windows stores the credentials cache (CC) in memory + and protects this area against unauthorized applications. But it is + neccessary to allow applications access to the CC to implement a real + single sign on solution. In detail applications need access to a + current, valid TGT which is stored in the + CC. + + + + Sun Java 1.6 should solve this problem by using the native Microsoft + libraries but in fact doesn't. Therefore the registry fix described in + described in and is needed. + + + + You also need to create a Kerberos configuration file. On Windows it is + named krb5.ini and is searched for in the Windows system + directory (e.g. C:\Windows). An example configuration is + in . + + + + Windows 2000 SP4, 2003 Server and later or Vista + + + Add under + HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\Kerberos\Parameters + the DWORD key AllowTGTSessionKey + with the value 1. + + + + HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\Kerberos\Parameters + Value Name: AllowTGTSessionKey + Value Type: REG_DWORD + Value: 1 + + + + + Windows XP + + + Add under + HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\Kerberos + the DWORD key AllowTGTSessionKey + with the value 1. + + + + HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\Kerberos + Value Name: AllowTGTSessionKey + Value Type: REG_DWORD + Value: 1 + + + + + + + JDBC Configuration and Behaviour + + + You can use the JDBC driver without supplying a user + name and password. It will initialize the Kerberos environment, + extract the user name from the principal name of your + TGT (without realm and instance) and authenticate + with it. + + + + If you supply a user name, the TGT with that user name is searched and + used. If it can not be found, an exception is thrown. + + + + It is possible to define another service name for the Kerberos server + (default: postgres). This should not be neccessary. + + + + The JDBC driver honors the setting of a keytab to + initialize the Kerberos stuff. If useKeyTab is not + defined or false, the driver tries to access the + credentials cache of the current user. For more information take a look + at the documentation of Krb5LoginModule. + + + Issuing a Query and Processing the Result diff -Nur postgresql-jdbc-8.3-604.src.orig/org/postgresql/core/v3/ConnectionFactoryImpl.java postgresql-jdbc-8.3-604.src/org/postgresql/core/v3/ConnectionFactoryImpl.java --- postgresql-jdbc-8.3-604.src.orig/org/postgresql/core/v3/ConnectionFactoryImpl.java 2008-01-08 07:56:27.000000000 +0100 +++ postgresql-jdbc-8.3-604.src/org/postgresql/core/v3/ConnectionFactoryImpl.java 2009-07-01 13:32:08.211901000 +0200 @@ -24,6 +24,7 @@ import org.postgresql.util.ServerErrorMessage; import org.postgresql.util.UnixCrypt; import org.postgresql.util.MD5Digest; +import org.postgresql.util.GSSHelper; import org.postgresql.util.GT; /** @@ -39,6 +40,12 @@ private static final int AUTH_REQ_CRYPT = 4; private static final int AUTH_REQ_MD5 = 5; private static final int AUTH_REQ_SCM = 6; + private static final int AUTH_REQ_GSS = 7; /* GSSAPI without wrap() */ + private static final int AUTH_REQ_GSS_CONT = 8; /* Continue GSS exchanges */ + private static final int AUTH_REQ_SSPI = 9; /* SSPI negotiate without wrap() */ + + // helper-object for GSS-API stuff + private GSSHelper gssHelper = null; /** Marker exception; thrown when we want to fall back to using V2. */ private static class UnsupportedProtocolException extends IOException { @@ -80,6 +87,27 @@ if (trySSL) newStream = enableSSL(newStream, requireSSL, info, logger); + // try to initialize GSS API but don't fail here (we don't know if + // the server will ask us for GSS authentication...): + try + { + gssHelper = new GSSHelper(host, user, info, logger); + + // if no user name was supplied, we'll take it from the + // currently logged in user + if (user == null || user.length() == 0) + { + user = gssHelper.getUserName(); + if (logger.logDebug()) + logger.debug("using user name " + user); + } + } + catch (PSQLException e) + { + if (logger.logDebug()) + logger.debug("failed to initialize GSS API: " + e.toString()); + } + // Construct and send a startup packet. String[][] params = { { "user", user }, @@ -92,7 +120,7 @@ sendStartupPacket(newStream, params, logger); // Do authentication (until AuthenticationOk). - doAuthentication(newStream, user, info.getProperty("password"), logger); + doAuthentication(newStream, info, user, info.getProperty("password"), logger); // Do final startup. ProtocolConnectionImpl protoConnection = new ProtocolConnectionImpl(newStream, user, database, info, logger); @@ -244,8 +272,10 @@ pgStream.flush(); } - private void doAuthentication(PGStream pgStream, String user, String password, Logger logger) throws IOException, SQLException + private void doAuthentication(PGStream pgStream, Properties info, String user, String password, Logger logger) throws IOException, SQLException { + boolean usingGSS = false; + // Now get the response from the backend, either an error message // or an authentication request @@ -363,9 +393,73 @@ break; } + case AUTH_REQ_GSS: + { + if (logger.logDebug()) + logger.debug(" <=BE AuthenticationReqGss"); + + if (gssHelper == null) + throw new PSQLException( + GT.tr( + "GSS authentication requested by server but GSS API not initialized."), + PSQLState.CONNECTION_UNABLE_TO_CONNECT); + + usingGSS = true; + + byte[] token = null; + token = gssHelper.establishContext(token); + if (token != null) { + if (logger.logDebug()) + logger.debug(" FE=> GSS token (" + token.length + " bytes)"); + pgStream.SendChar('p'); + pgStream.SendInteger4(token.length + 4); + pgStream.Send(token); + pgStream.flush(); + } + + break; + } + + case AUTH_REQ_GSS_CONT: + { + if (logger.logDebug()) + logger.debug(" <=BE AuthenticationReqGssCont"); + + if (gssHelper == null) + throw new PSQLException( + GT.tr( + "GSS authentication continuation requested but GSS API not initialized."), + PSQLState.CONNECTION_UNABLE_TO_CONNECT); + + // token length = msg_len - sizeof(msg_len) - sizeof(auth_type) + byte[] token = new byte[l_msgLen - 8]; + pgStream.Receive(token, 0, token.length); + + if (logger.logDebug()) + logger.debug(" <=BE GSS token (" + token.length + " bytes)"); + + token = gssHelper.establishContext(token); + if (token != null) { + if (logger.logDebug()) + logger.debug( " FE=> GSS token (" + token.length + " bytes)"); + pgStream.SendChar('p'); + pgStream.SendInteger4(token.length + 4); + pgStream.Send(token); + pgStream.flush(); + } + + break; + } + case AUTH_REQ_OK: if (logger.logDebug()) logger.debug(" <=BE AuthenticationOk"); + + // throw an exception if the GSS API context is not completely established + if (usingGSS && !gssHelper.isContextEstablished()) + { + throw new PSQLException(GT.tr("The GSS context between server and client is not fully established. Maybe the server is compromised."), PSQLState.CONNECTION_FAILURE); + } return ; // We're done. diff -Nur postgresql-jdbc-8.3-604.src.orig/org/postgresql/util/GSSHelper.java postgresql-jdbc-8.3-604.src/org/postgresql/util/GSSHelper.java --- postgresql-jdbc-8.3-604.src.orig/org/postgresql/util/GSSHelper.java 1970-01-01 01:00:00.000000000 +0100 +++ postgresql-jdbc-8.3-604.src/org/postgresql/util/GSSHelper.java 2009-07-01 15:05:51.123096000 +0200 @@ -0,0 +1,296 @@ +/*------------------------------------------------------------------------- + * GSSHelper.java + * + * Author: Christian Jung + */ +package org.postgresql.util; + +import com.sun.security.auth.module.Krb5LoginModule; +import java.lang.RuntimeException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.StringTokenizer; +import javax.security.auth.Subject; +import javax.security.auth.kerberos.KerberosTicket; +import javax.security.auth.login.LoginException; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; +import org.postgresql.core.Logger; +import org.postgresql.util.GT; +import org.postgresql.util.PSQLException; +import org.postgresql.util.PSQLState; + +/** + * This class implements methods used for the GSS API authentication protocol + * on a PostgreSQL. + * + * @author Christian Jung (christian.jung@saarstahl.com) + */ +public class GSSHelper extends Object +{ + // constants: + private static final String PG_KRB_SRV_NAM = "postgres"; + private static final org.ietf.jgss.Oid OID_KRB5_MECH; + + // initialize complex class objects: + static + { + try + { + OID_KRB5_MECH = new org.ietf.jgss.Oid( + "1.2.840.113554.1.2.2"); + } + catch (GSSException e) + { + throw new RuntimeException(e); + } + } + + private Subject subject = null; // object which stores the private credentials of the local user + private GSSContext context = null; // GSS context + private String userPrincipalName = null; // principal name of the local user + private String krbServiceName = null; // service-part of server's principal name + private String fqdn = null; // full qualified domain name of the server + + + /** + * Creates a new GSSHelper object. + * + * It is used in the ConnectionFactory implementation. The constructor + * intializes the Kerberos credentials cache and the GSS API layer needed + * to authenticate via GSS. + * + * @param host hostname of the server to be connected to + * @param user user name (may be null, a Kerberos principal or a Postgres user) + * @param info driver configuration + * @param logger logger which will be used to log + * @exception PSQLException on error. All exceptions which are catched in + * the constructor are passed as the cause of the PSQLException. + */ + public GSSHelper(String host, String user, Properties info, Logger logger) throws PSQLException + { + krbServiceName = info.getProperty("krbsrvname", PG_KRB_SRV_NAM); + + subject = new Subject(); + + Map krb5LoginOptions = new HashMap(); + krb5LoginOptions.put("doNotPrompt", "true"); + // set useTicketCache only if useKeyTab is "false" + if (System.getProperty("useKeyTab", "false").equalsIgnoreCase("false") && + info.getProperty("useKeyTab", "false").equalsIgnoreCase("false")) + krb5LoginOptions.put("useTicketCache", "true"); + + // initialize kerberos stuff: + try + { + Krb5LoginModule krb5Login = new Krb5LoginModule(); + krb5Login.initialize(subject, null, null, krb5LoginOptions); + krb5Login.login(); + krb5Login.commit(); + } + catch (LoginException e) + { + throw new PSQLException( + GT.tr("failed to initialize kerberos environment ({0})", + e.toString()), PSQLState.CONNECTION_UNABLE_TO_CONNECT, e); + } + + // check if we have a valid TGT: + Set privateCredentials = subject.getPrivateCredentials(KerberosTicket.class); + KerberosTicket krb5Tgt = null; + for (Iterator i = privateCredentials.iterator(); i.hasNext();) + { + KerberosTicket ticket = (KerberosTicket)i.next(); + + if (ticket.isInitial() && ticket.isCurrent()) + { + if (logger.logDebug()) + logger.debug("found valid TGT of user " + ticket.getClient().getName()); + + // check if the TGT is from the supplied user: + if (user != null && user.length() > 0) + { + String principalName = new StringTokenizer( + ticket.getClient().getName().toLowerCase(), "/@").nextToken(); + + if (!user.toLowerCase().equals(principalName)) + { + logger.info("TGT principal name does not match requested user"); + continue; + } + } + + krb5Tgt = ticket; + break; + } + } + + if (krb5Tgt == null) + throw new PSQLException( + GT.tr("no valid TGT found"), + PSQLState.CONNECTION_UNABLE_TO_CONNECT); + + userPrincipalName = krb5Tgt.getClient().getName(); + + // get the full qualified domain name of the database server + try + { + fqdn = InetAddress.getByName(host).getCanonicalHostName(); + } + catch (UnknownHostException e) + { + throw new PSQLException( + GT.tr("failed to get full qualified domain name of {0}", host), + PSQLState.CONNECTION_UNABLE_TO_CONNECT, e); + } + + // intialize the GSS API + try + { + context = (GSSContext)Subject.doAsPrivileged( + subject, + new PrivilegedExceptionAction() + { + public Object run() throws GSSException + { + GSSManager manager = GSSManager.getInstance(); + + GSSName gssPeerName = manager.createName( + krbServiceName + "@" + fqdn, + GSSName.NT_HOSTBASED_SERVICE); + + GSSName gssUserName = manager.createName( + userPrincipalName, + GSSName.NT_USER_NAME); + + GSSCredential userCredential = manager.createCredential( + gssUserName, + GSSCredential.DEFAULT_LIFETIME, + OID_KRB5_MECH, + GSSCredential.INITIATE_ONLY); + + return manager.createContext( + gssPeerName, + OID_KRB5_MECH, + userCredential, + GSSContext.DEFAULT_LIFETIME); + } + }, null); + + // enable mutual authentication: + context.requestMutualAuth(true); + + // disable confidentiality and integrity checking after + // context establishment (this is not needed in our case) + context.requestConf(false); + context.requestInteg(false); + } + catch (GSSException e) + { + throw new PSQLException( + GT.tr("GSS API initialization failed: {0}", e.toString()), + PSQLState.CONNECTION_UNABLE_TO_CONNECT, e); + } + catch (PrivilegedActionException e) + { + GSSException cause = (GSSException)e.getCause(); + + throw new PSQLException( + GT.tr("GSS API initialization failed: {0}", cause.toString()), + PSQLState.CONNECTION_UNABLE_TO_CONNECT, e); + } + } + + + private byte[] token; + /** + * Establish the GSS API context between client an server. + * + * An authentication via GSS API is done by exchanging tokens between + * client and server until all authentication issues are met. In our case + * these are normally only two tokens. One to the server and one from the + * server (mutual authentication). + * + * The first call of this method may be done with null or an empty byte + * array as token. Successive calls have to be made with the token send + * from the server. + * + * @param in token received from the server, null or empty byte array + * @return token which has to be send to the server + * @exception PSQLException on error. All exceptions which are catched in + * this method are passed as the cause of the PSQLException. + */ + public byte[] establishContext( byte[] in ) throws PSQLException + { + token = (in == null ? new byte[0] : in); + + try + { + Subject.doAsPrivileged(subject, new PrivilegedExceptionAction() + { + public Object run() throws GSSException + { + if (!context.isEstablished()) + token = context.initSecContext(token, 0, token.length); + + return (token == null ? 0 : token.length); + } + }, null); + } + catch (PrivilegedActionException e) + { + GSSException cause = (GSSException)e.getCause(); + + throw new PSQLException( + GT.tr("GSS error occured: {0}", cause.toString()), + PSQLState.CONNECTION_UNABLE_TO_CONNECT, e); + } + + return token; + } + + + /** + * Returns whether the GSS context between server and client is + * established. + * + * @return true if the context is established otherwise false + */ + public boolean isContextEstablished() + { + return (context != null ? context.isEstablished() : false); + } + + + /** + * Returns the Postgres user name of local principal name. + * + * The user name is derived from the principal name by stripping any + * instance and realm from it. + * + * 'cju/admin@EXAMPLE.ORG' is converted to 'cju' for example. + * + * @return Postgres user name + */ + public String getUserName() + { + if (userPrincipalName == null) + return null; + + // return only the user from the principal name (analogous to + // C-function pg_an_to_ln()) + return new StringTokenizer(userPrincipalName, "/@").nextToken(); + + } +}