Search code examples
javac#active-directoryldapkerberos

How we can directly use kerberos ldap service ticket to authenticate with ldap and form the LDAP context


Basic Question:

Can I use service ticket generated for ldap service elsewhere, in my java to authenticate using kerberos to the ldap service and form the LdapContext for further operations?

Goal

I want to implement password less authentication for my Java service running on Unix machine to connect to Active Directory Ldap to perform various CRUD operations on the directory objects.

Note: For various reasons I cannot use keytab file and later on I want to extend this to use the gMSAs to make it purely password less authentication. By password less, I mean to say I want to avoid explicit password management on DC and for Java service running on Unix, it has to be passwordless

My Idea :

  1. Use a Windows Agent Service that can successfully generate a service ticket for ldap
  2. Encode the service ticket (base64) and provide it to the Java Service (securely)
  3. Java can then decode it and use it to authenticate to ldap and form the LdapContext for further usage
  4. The communication between java service and the windows agent service is secure and Java service can periodically ask the windows service to generate the ldap service tickets.
  5. Now with this, I am removing the dependency of password at Java service to begin with
  6. The password information will only be the windows agent service at this moment.
  7. Later on when I integrate gMSA, then even the requirement for password at the windows agent side would be gone, thus achieving my goal!

Firstly, I want to validate whether this can be theoretically possible.

What I have tried till now?

Using Kerberos.NET C# library to generate the ldap service ticket

This C# generates the ldap service ticket:

    private static async Task<string> GenerateLDAPServiceTicketForUser(string userName, string password, string domain, string ldapSPN)
    {
        KerberosClient client = new KerberosClient();
        await client.Authenticate(new KerberosPasswordCredential(userName, password, domain));     
        KrbApReq ticket = await client.GetServiceTicket(ldapSPN);
        if (null != ticket)
        {
            
            ReadOnlyMemory<byte> memory = ticket.EncodeApplication();
            byte[] ticketBytes = memory.ToArray();
            return Convert.ToBase64String(ticketBytes);
        } else
        {             
            throw new Exception("Ticket is null...Service Ticket could not be generated...");
        }
    }

The input provided to the function is:

 **username**: Administrator
 **domain**: helix.lab
 **ldapSPN**: ldap/WIN-FMCLF26TASJ.Helix.Lab
 **password**: <my-password>

This successfully generates a base64 encoded service ticket.

Now I am trying to use the same in my java application to connect the helix.lab's ldap service and perform ldap operations. This PoC just demonstrates how we can use already service ticket to connect to AD ldap service (and prove my hypothesis of password less connection at java service side)

Here is my Java Code:

Initialisation

System.setProperty("java.security.krb5.conf", "/etc/krb5.conf");
System.setProperty("sun.security.krb5.debug", "true");
System.setProperty("sun.security.jgss.debug", "true");

Decode Base64 ticket and form the KerberosTicket

String base64Ticket = "<my base 64 encoded ticket>";
byte[] ticketBytes = Base64.getDecoder().decode(base64Ticket);
byte[] sessionKey = new byte[] {0x00, 0x01, 0x02};

Date authTime = new Date(System.currentTimeMillis());
Date startTime = new Date(System.currentTimeMillis());
Date endTime = new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000);
Date renewTill = new Date(System.currentTimeMillis() + 48 * 60 * 60 * 1000);;

KerberosTicket ticket = new KerberosTicket(ticketBytes,
        new KerberosPrincipal("[email protected]"),
        new KerberosPrincipal("ldap/WIN-FMCLF26TASJ.Helix.Lab"),
        sessionKey, 18, null, authTime, startTime, endTime, renewTill, null);

Subject subject = new Subject();
subject.getPrivateCredentials().add(ticket);

Connect to LDAP

Subject.doAs(subject, new PrivilegedAction<Void>() {

    @Override
    public Void run() {
        connectToLdap();
        return null;
    }
});

Implement connecToLdap:

String ldapURL = "ldap://WIN-FMCLF26TASJ.helix.lab:389";
Hashtable<String, Object> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, ldapURL);
env.put(Context.SECURITY_AUTHENTICATION, "GSSAPI");
env.put(Context.SECURITY_PRINCIPAL, "ldap/[email protected]");

try {
    LdapContext ctx = new InitialLdapContext(env, null);
    // Do ldap operations....
} catch(Exception e) {
}

The setspn -l command on my DC gives following output (multiple entries for ldap):

ldap/WIN-FMCLF26TASJ/HELIX
ldap/b57bcf7f-d8b5-4e38-a2d2-f6833fe3d617._msdcs.Helix.lab
ldap/WIN-FMCLF26TASJ.Helix.lab/HELIX
ldap/WIN-FMCLF26TASJ
ldap/WIN-FMCLF26TASJ.Helix.lab
ldap/WIN-FMCLF26TASJ.Helix.lab/Helix.lab 

Out of that I am using: ldap/WIN-FMCLF26TASJ.Helix.lab which I thought made more sense.

When I run the java code: I am getting following error:

Java config name: /etc/krb5.conf
Loading krb5 profile at /etc/krb5.conf
Loaded from Java config
Search Subject for Kerberos V5 INIT cred (<<DEF>>, 
sun.security.jgss.krb5.Krb5InitCredential)
Found ticket for [email protected] to go to ldap/[email protected] expiring on Tue Apr 23 14:01:51 IST 2024
Error occurred: GSSAPI exception:  javax.naming.AuthenticationException: GSSAPI [Root 
exception is javax.security.sasl.SaslException: GSS initiate failed [Caused by 
GSSException: No valid credentials provided (Mechanism level: Identifier doesn't match 
expected value (906))]]

What does this line indicate?

Found ticket for [email protected] to go to ldap/[email protected] expiring on Tue Apr 23 14:01:51 IST 2024

Why did authentication fail? What wrong am I doing or what am I missing to understand? Any other approach or suggestion might help me but would like to understand what is going wrong here and how can I rectify it?


Update:

This is what the ticket looks like after decoding it on https://lapo.it/asn1js

enter image description here

Update 2 : Service Ticket retrieved from AP-REQ enter image description here

Update 3:

Upon adding the valid ticket and its cipher, getting following error: 
Java config name: /etc/krb5.conf
Loading krb5 profile at /etc/krb5.conf
Loaded from Java config
Search Subject for Kerberos V5 INIT cred (<<DEF>>, sun.security.jgss.krb5.Krb5InitCredential)
Found ticket for [email protected] to go to ldap/[email protected] expiring on Wed Apr 24 13:17:10 IST 2024
Entered Krb5Context.initSecContext with state=STATE_NEW
Found ticket for [email protected] to go to ldap/[email protected] expiring on Wed Apr 24 13:17:10 IST 2024
Service ticket not found in the subject
>>> serviceCredsSingle: cross-realm authentication
>>> serviceCredsSingle: obtaining credentials from WIN-FMCLF26TASJ.Helix.Lab to HELIX.LAB
>>> Credentials acquireServiceCreds: main loop: [0] tempService=krbtgt/[email protected]
>>> Credentials serviceCredsSingle: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 20 19.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> Credentials serviceCredsSingle: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 20 19.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> Credentials acquireServiceCreds: no tgt; searching thru capath
>>> Credentials acquireServiceCreds: inner loop: [1] tempService=krbtgt/[email protected]
>>> Credentials serviceCredsSingle: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 20 19.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> Credentials serviceCredsSingle: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 20 19.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> Credentials acquireServiceCreds: inner loop: [2] tempService=krbtgt/[email protected]
>>> Credentials serviceCredsSingle: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 20 19.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> Credentials serviceCredsSingle: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 20 19.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> Credentials acquireServiceCreds: inner loop: [3] tempService=krbtgt/[email protected]
>>> Credentials serviceCredsSingle: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 20 19.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> Credentials serviceCredsSingle: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 20 19.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> Credentials acquireServiceCreds: no tgt; cannot get creds
>>> serviceCredsSingle: cross-realm authentication
>>> serviceCredsSingle: obtaining credentials from WIN-FMCLF26TASJ.Helix.Lab to HELIX.LAB
>>> Credentials acquireServiceCreds: main loop: [0] tempService=krbtgt/[email protected]
>>> Credentials serviceCredsSingle: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 20 19.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> Credentials serviceCredsSingle: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 20 19.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> Credentials acquireServiceCreds: no tgt; searching thru capath
>>> Credentials acquireServiceCreds: inner loop: [1] tempService=krbtgt/[email protected]
>>> Credentials serviceCredsSingle: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 20 19.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> Credentials serviceCredsSingle: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 20 19.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> Credentials acquireServiceCreds: inner loop: [2] tempService=krbtgt/[email protected]
>>> Credentials serviceCredsSingle: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 20 19.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> Credentials serviceCredsSingle: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 20 19.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> Credentials acquireServiceCreds: inner loop: [3] tempService=krbtgt/[email protected]
>>> Credentials serviceCredsSingle: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 20 19.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> Credentials serviceCredsSingle: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 20 19.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> Credentials acquireServiceCreds: no tgt; cannot get creds
KrbException: Fail to create credential. (63) - No service creds
    at java.security.jgss/sun.security.krb5.internal.CredentialsUtil.serviceCredsSingle(CredentialsUtil.java:458)
    at java.security.jgss/sun.security.krb5.internal.CredentialsUtil.serviceCreds(CredentialsUtil.java:340)
    at java.security.jgss/sun.security.krb5.internal.CredentialsUtil.serviceCreds(CredentialsUtil.java:314)
    at java.security.jgss/sun.security.krb5.internal.CredentialsUtil.acquireServiceCreds(CredentialsUtil.java:169)
    at java.security.jgss/sun.security.krb5.Credentials.acquireServiceCreds(Credentials.java:490)
    at java.security.jgss/sun.security.jgss.krb5.Krb5Context.initSecContext(Krb5Context.java:697)
    at java.security.jgss/sun.security.jgss.GSSContextImpl.initSecContext(GSSContextImpl.java:266)

Solution

  • With the help of @user1686 I was able to affirm that what I wanted to achieve can indeed be done!

    C# code to get the base64 encoded session key and ldap service ticket (Note I have used Kerberos.NET library):

    KerberosClient client = new KerberosClient();
    string principal = userName + "@" + domain.ToUpper();
    KerberosPasswordCredential kpc = new KerberosPasswordCredential(principal, password);
                
    await client.Authenticate(kpc);
    KerberosClientCacheEntry entry = client.Cache.GetCacheItem<KerberosClientCacheEntry>("krbtgt/EXAMPLE.LAB");
    
    KrbApReq ticket = await client.GetServiceTicket(ldapSPN); // get the service ticket for the ldap SPN
    KrbTicket serviceTkt = ticket.Ticket;
                
    KerberosClientCacheEntry c2 = client.Cache.GetCacheItem<KerberosClientCacheEntry>(ldapSPN);
    
    ReadOnlyMemory<byte> sessionKeyValue = c2.SessionKey.KeyValue;
    byte[] sessionKeyValueBytes = sessionKeyValue.ToArray();
                
    Console.WriteLine("LDAP Service Ticket Session Key : " + Convert.ToBase64String(sessionKeyValueBytes));           Console.WriteLine(Convert.ToBase64String(serviceTkt.EncodeApplication().ToArray()));
    

    Now that we have the session key and the service ticket, this is how I reconstructed it in my Java code:

    System.setProperty("java.security.krb5.conf", "/etc/krb5.conf");
    System.setProperty("sun.security.krb5.debug", "true");
    System.setProperty("sun.security.jgss.debug", "true");
    
    
    String base64cipher = "<base64-session-key>";
    String base64Ticket = "<base64-ticket>";
    
    byte[] ticketBytes = Base64.getDecoder().decode(base64Ticket);
    byte[] sessionKey = Base64.getDecoder().decode(base64cipher);
    
    Date authTime = new Date(System.currentTimeMillis());
    Date startTime = new Date(System.currentTimeMillis());
    Date endTime = new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000);
    Date renewTill = new Date(System.currentTimeMillis() + 48 * 60 * 60 * 1000);
    
    
    KerberosTicket ticket = new KerberosTicket(ticketBytes,
                    new KerberosPrincipal("[email protected]"),
                    new KerberosPrincipal("ldap/[email protected]"),
                    sessionKey, 18, null, authTime, startTime, endTime, renewTill, null);
    
    Subject subject = new Subject();
    subject.getPrincipals().add(new KerberosPrincipal("[email protected]"));
            subject.getPrivateCredentials().add(ticket);
            Subject.doAs(subject, new PrivilegedAction<Void>() {
    
                @Override
                public Void run() {
                    connectToLdap();
                    return null;
                }
            });
    

    This is the implementation for connectToLdap()

    String ldapURL = "ldap://hostname.example.lab:389";
    Hashtable<String, Object> env = new Hashtable<>();
    env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
    env.put(Context.PROVIDER_URL, ldapURL);
    env.put(Context.SECURITY_AUTHENTICATION, "GSSAPI");
    
    LdapContext ctx = new InitialLdapContext(env, null);
    

    I was able to form the LdapContext and perform directory operations using the context as well