JDK-8180144 : JGSS API should retrieve Kerberos Service Tickets (SGTs) from the credential cache and add to the Subject Principal private credentials
  • Type: Enhancement
  • Component: security-libs
  • Sub-Component: org.ietf.jgss
  • Affected Version: 8,9
  • Priority: P4
  • Status: Closed
  • Resolution: Won't Fix
  • OS: linux_ubuntu
  • CPU: x86_64
  • Submitted: 2017-05-10
  • Updated: 2021-02-10
  • Resolved: 2021-02-10
Description
A DESCRIPTION OF THE REQUEST :
As Is:
When JGSS or JAAS setups a Subject for a Kerberos Client Principal (and the appropriate config settings are made), a Ticket Granting Ticket (TGT) is retrieved from the credentials cache and stored for later use in the private credentials of the Subject. See sun.security.krb5.Credentials.acquireTGTFFromCache(). --> OK.

Later, when GSSContext.initSecContext() is called, and a Kerberos Service Ticket (SGT) is required, the security code tries to retrieve a SGT from private credentials of the Subject. As SGT(s) were never saved in the subject, they can never be found there. As a result a new request is made to the KDC.  --> NOT OK.

See sun.security.jgss.krb5.Krb5Context.initSecContext() / KrbUtil.getTicket() / SubjectComber.find() / findAux().

To Be:
Both TGTs and SGTs should be loaded from the credential cache to the subject private credentials for later use  in GSSContext.initSecContext(). Possibly the type of tickets loaded and stored could be steered by configuration.

As a "nice-to-have", where a new TGT is acquired (because there is none in the credentials cache), it should be written back to the credentials cache for future use.

Note this behaviour was observed / debugged on Lubuntu Linux 17.04 using Java 8 u 131, but is likely to be common to other Operating Systems (OS) that provide Kerberos Credential Caches. The type of caches will vary from OS to OS.




JUSTIFICATION :
As the SGT cannot be found in the Subject's private credentials (as it was never stored there), each use of a SGT results in a call for a new SGT to the Kerberos Key Distribution Center (KDC), despite the presence of a valid SGT in the cache. 

This can lead to the KDC being hammered. The whole point of storing SGTs in a credential cache is to avoid this happening.

Consequence:

a) Java GSS-API can only be used for initial login of client application to the Kerberos Service Principal. All further service calls make use of an alternative security mechanism (some kind of home-brewed Kerberos light) using tokens / cookies.

or

b) use an alternative Java Kerberos client implementation .... This has implications also for the Service Principal which also uses GSS API

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
To Be:
Both TGTs and SGTs should be loaded from the credential cache to the subject private credentials for later use  in GSSContext.initSecContext() 

As a "nice-to-have", where a new TGT is acquired (because there is none in the credentials cache), it should be written back to the credentials cache for future use.
ACTUAL -
As Is:
When JGSS or JAAS setups a Subject for a Kerberos Client Principal (and the appropriate config settings are made), a Ticket Granting Ticket (TGT) is retrieved from the credentials cache and stored for later use in the private credentials of the Subject. See sun.security.krb5.Credentials.acquireTGTFFromCache(). --> OK.

Later, when GSSContext.initSecContext() is called, and a Kerberos Service Ticket (SGT) is required, the security code tries to retrieve a SGT from private credentials of the Subject. As SGT(s) were never saved in the subject, they can never be found there. As a result a new request is made to the KDC.  --> NOT OK.

See sun.security.jgss.krb5.Krb5Context.initSecContext() / KrbUtil.getTicket() / SubjectComber.find() / findAux().

---------- BEGIN SOURCE ----------
  private String getGSSwJAASServiceTicket()  {
    
    byte[] ticket = null;
    String encodedTicket = null;
    LoginContext loginContext = null;
    Subject mySubject =  null;
    
    try {
      TextCallbackHandler cbHandler = new TextCallbackHandler();
      loginContext = new LoginContext("wSOXClientGSSJAASLogin", cbHandler);
      loginContext.login();
      mySubject = loginContext.getSubject();
      
      GSSManager manager = GSSManager.getInstance();
      
      GSSName serverName = manager.createName("HTTP/app-srv.acme.com@ACME.COM", null);
      Oid krb5Oid = new Oid("1.2.840.113554.1.2.2");
      
     
      ticket = Subject.doAs(mySubject, new PrivilegedAction<byte[]>(){
        public byte[] run(){
          try{
            
            System.setProperty("javax.security.auth.useSubjectCredsOnly","true");
            GSSContext context = manager.createContext(serverName,
                krb5Oid,
                null,
                GSSContext.DEFAULT_LIFETIME);
            
            context.requestMutualAuth(false);
            context.requestConf(false);
            context.requestInteg(true);
            
            byte[] token = new byte[0];     
            return context.initSecContext(token, 0, token.length);
            
          }
          catch(Exception e){
            Log.log(Log.ERROR, e);
            throw new otms.util.OTMSRuntimeException("Start wSOXclient (privileged) failed, cause: " + e.getMessage());
          }
        }
      });
    } catch (LoginException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (GSSException e1) {
      // TODO Auto-generated catch block
      e1.printStackTrace();
    }
    encodedTicket = Base64.getEncoder().encodeToString(ticket);
    return encodedTicket;
  }
---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
  private String getGSSwJAASServiceTicket()  {
    
    byte[] ticket = null;
    String encodedTicket = null;
    LoginContext loginContext = null;
    Subject mySubject =  null;
    
    try {
      TextCallbackHandler cbHandler = new TextCallbackHandler();
      loginContext = new LoginContext("wSOXClientGSSJAASLogin", cbHandler);
      loginContext.login();
      mySubject = loginContext.getSubject();
      
      /* Nasty work-around, for proof-of-concept and demo purposes only, as uses sun internal apis
      // assumes 1 TGT and 1 SGT exist in credentials cache (added using python py-curl + GSS)
      // Demos that if SGTs are stored in the subject's privateCredentials, they can be accessed during context.initSecContext() below, without a call to the KDC.
      sun.security.krb5.internal.ccache.CredentialsCache ccache = sun.security.krb5.internal.ccache.CredentialsCache.getInstance("/tmp/krb5cc_9337");
      sun.security.krb5.internal.ccache.Credentials[] creds = ccache.getCredsList();    
      mySubject.getPrivateCredentials().add(sun.security.jgss.krb5.Krb5Util.credsToTicket(creds[0].setKrbCreds()));
      mySubject.getPrivateCredentials().add(sun.security.jgss.krb5.Krb5Util.credsToTicket(creds[1].setKrbCreds()));
      */ End Nasty work-around
    
      GSSManager manager = GSSManager.getInstance();
      
      GSSName serverName = manager.createName("HTTP/app-srv.acme.com@ACME.COM", null);
      Oid krb5Oid = new Oid("1.2.840.113554.1.2.2");
      
     
      ticket = Subject.doAs(mySubject, new PrivilegedAction<byte[]>(){
        public byte[] run(){
          try{
            
            System.setProperty("javax.security.auth.useSubjectCredsOnly","true");
            GSSContext context = manager.createContext(serverName,
                krb5Oid,
                null,
                GSSContext.DEFAULT_LIFETIME);
            
            context.requestMutualAuth(false);
            context.requestConf(false);
            context.requestInteg(true);
            
            byte[] token = new byte[0];     
            return context.initSecContext(token, 0, token.length);
            
          }
          catch(Exception e){
            Log.log(Log.ERROR, e);
            throw new otms.util.OTMSRuntimeException("Start wSOXclient (privileged) failed, cause: " + e.getMessage());
          }
        }
      });
    } catch (LoginException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (GSSException e1) {
      // TODO Auto-generated catch block
      e1.printStackTrace();
    }
    encodedTicket = Base64.getEncoder().encodeToString(ticket);
    return encodedTicket;
  }


Comments
The JAAS/JGSS model requires a TGT before a service ticket. Do not change this model.
10-02-2021