Reputation: 1098
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("[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
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 [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
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
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