theimpatientcoder
theimpatientcoder

Reputation: 1098

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)

Upvotes: 1

Views: 476

Answers (2)

theimpatientcoder
theimpatientcoder

Reputation: 1098

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

Upvotes: 1

grawity_u1686
grawity_u1686

Reputation: 16122

Identifier doesn't match expected value (906)

This seems to be the Java Kerberos error code for ASN.1 decoding errors – i.e. it found a different ASN.1 field than it expected. This looks like a similar issue to https://github.com/dotnet/Kerberos.NET/issues/49, i.e. you might be getting the wrong structure as the "ticket" from the C# code.

I'm not sure exactly what the mismatch is, so first I'd dump the ticket bytes into a file and run it through any ASN.1 DER decoder (e.g. dumpasn1 or openssl asn1parse -i -inform DER or even asn1js) and compare it to what a normal ticket should look like.

$ cat ticket.bin | ./dumpasn1
  0 594: [APPLICATION 1] {
  4 590:   SEQUENCE {
  8   3:     [0] {
 10   1:       INTEGER 5
       :       }
 13  14:     [1] {
 15  12:       GeneralString 'HELIX.LAB'
       :       }
 29  27:     [2] {
 31  25:       SEQUENCE {
 33   3:         [0] {
 35   1:           INTEGER 3
       :           }
 38  18:         [1] {
 40  16:           SEQUENCE {
 42   4:             GeneralString 'ldap'
 48   8:             GeneralString 'WIN-FMCLF26TASJ.Helix.lab'

If your ticket looks like this, the problem is on the Java side; if it looks different, the problem is with the way you extract it on the C# side.

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

That's definitely not a ticket. That's the actual authentication token that would be sent to the LDAP server.

(Actually, the KrbApReq type definition in your C# code already kind-of implies that this would be the case, but it goes even further – the message isn't even an AP-REQ, it's a SPNEGO token that has an AP-REQ inside, and the real ticket is some 5 layers of data structures inside.)

Roughly:

GSSAPI SPNEGO token          (starts with the 1.3.6.1.5.5.2 OID)
└ GSSAPI Kerberos token      (starts with the 1.2.840.113554.1.2.2 OID)
  └ Kerberos AP-REQ message  (starts at the [APPLICATION 14] tag)
    ├ Kerberos ticket        (starts at the [APPLICATION 1] tag)
    └ Authenticator

So the problem is definitely on the side of the C# agent. I have no experience with Kerberos.NET, though, so I don't know what the correct function to call is.

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

Yes, ldap/<fqdn> is the standard one outside of AD, but keep in mind that non-AD Kerberos is usually case-sensitive, unlike SSPI – the SPN you're using for the GSSAPI auth needs to exactly match the SPN on the ticket.

Why did authentication fail? What wrong am I doing

At first glance (I'm not familiar with AD specifics), I see only one issue but it's a major one: You're exporting only the ticket but not the session key. Those two are inseparable, as the ticket itself carries a second (encrypted) copy of the key that the server will decrypt.

The ticket alone is not enough for authentication (and Kerberos pretty much treats it as "public" data). Every time you authenticate, the client must generate an one-time "authenticator" using the session key, and must send both the ticket and the authenticator to the server. (The ticket+session key can be reused, but the authenticator is single-use.)

Defining the session key as new byte[] {0x00, 0x01, 0x02} will therefore not work at all. (More so because that's too short for an AES key, as well.)

Related to the session key, you are currently hardcoding enctype 18 (aes256-hmac-sha1), but you're not checking anywhere that your ticket was in fact for enctype 18. This is another parameter you must export together with the ticket. (Although the ticket enctype and the session-key enctype have usually been the same in the past, they are not guaranteed to be.)

(I'm also not sure about your new KerberosPrincipal() calls. One of them is missing the @HELIX.LAB realm; you probably should include that explicitly unless you are 120% sure that Java will append the correct default realm.)


One missing part of your plan is the communications between the C# agent and the Java service. How exactly will tickets get from the agent on Windows system A to the service on Unix system B?

If the Unix service requests them from the agent over the network, then that request needs to be authenticated somehow – and you're back to needing to store some kind of password on the Unix service that lets it get tickets, exactly as if you had just used a static Kerberos keytab in the first place. Even if you're storing a TLS client certificate, that's still literally the same thing as a Kerberos keytab.

Upvotes: 1

Related Questions