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 :
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("Administrator@HELIX.LAB"),
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/WIN-FMCLF26TASJ.Helix.Lab@HELIX.LAB");
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 Administrator@HELIX.LAB to go to ldap/WIN-FMCLF26TASJ.Helix.lab@HELIX.LAB 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 Administrator@HELIX.LAB to go to ldap/WIN-FMCLF26TASJ.Helix.lab@HELIX.LAB 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
Update 2 : Service Ticket retrieved from AP-REQ
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 Admininstrator@HELIX.LAB to go to ldap/WIN-FMCLF26TASJ.Helix.Lab@HELIX.LAB expiring on Wed Apr 24 13:17:10 IST 2024
Entered Krb5Context.initSecContext with state=STATE_NEW
Found ticket for Admininstrator@HELIX.LAB to go to ldap/WIN-FMCLF26TASJ.Helix.Lab@HELIX.LAB 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/HELIX.LAB@WIN-FMCLF26TASJ.Helix.Lab
>>> 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/Helix.Lab@WIN-FMCLF26TASJ.Helix.Lab
>>> 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/Lab@WIN-FMCLF26TASJ.Helix.Lab
>>> 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/LAB@WIN-FMCLF26TASJ.Helix.Lab
>>> 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/HELIX.LAB@WIN-FMCLF26TASJ.Helix.Lab
>>> 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/Helix.Lab@WIN-FMCLF26TASJ.Helix.Lab
>>> 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/Lab@WIN-FMCLF26TASJ.Helix.Lab
>>> 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/LAB@WIN-FMCLF26TASJ.Helix.Lab
>>> 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)
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("administrator@HELIX.LAB"),
new KerberosPrincipal("ldap/win-fmclf26tasj.helix.lab@HELIX.LAB"),
sessionKey, 18, null, authTime, startTime, endTime, renewTill, null);
Subject subject = new Subject();
subject.getPrincipals().add(new KerberosPrincipal("administrator@HELIX.LAB"));
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