Description
Describe the bug
I'm in the process of writing a service that uses Kerberos.NET to process and validate Windows AD-issued Kerberos tickets without the service being part of the ticket issuing domain. My service must have the ability to accept tickets for multiple SPNs (for multiple hostnames), which is why I'm dealing with multiple keys exported through Windows' ktpass
command (one for each SPN, all of which have a dedicated service user for the secret in the ticket issuing domain). I've merged all of these keytab entries into a single keytab file using the MIT kerberos tooling (i.e. ktutil
).
What I'm observing with this setup is that the Kerberos.NET validator (created with the KeyTable
from said file) fails to select the appropriate key for the ticket I want to validate (see here). This causes KeyTable.GetKey()
to fall back to the first key with the required EncryptionType
or simply to the first key in the keytab file. In a keytab file where all keys have the same encryption type and the right key isn't the first entry, this leads to the validator complaining about an "invalid checksum":
System.Security.SecurityException: Invalid checksum
at Kerberos.NET.Crypto.AESTransformer.Decrypt(ReadOnlyMemory`1 cipher, KerberosKey kerberosKey, KeyUsage usage) in D:\a\1\s\Kerberos.NET\Crypto\AES\AESTransformer.cs:line 128
at Kerberos.NET.Entities.KrbEncryptedData.Decrypt[T](KerberosKey key, KeyUsage usage, Func`2 func) in D:\a\1\s\Kerberos.NET\Entities\Krb\KrbEncryptedData.cs:line 34
at Kerberos.NET.Crypto.DecryptedKrbApReq.Decrypt(KerberosKey ticketEncryptingKey) in D:\a\1\s\Kerberos.NET\Crypto\DecryptedKrbApReq.cs:line 75
at Kerberos.NET.Crypto.DecryptedKrbApReq.Decrypt(KeyTable keytab) in D:\a\1\s\Kerberos.NET\Crypto\DecryptedKrbApReq.cs:line 70
at Kerberos.NET.Entities.ContextToken.DecryptApReq(KrbApReq token, KeyTable keytab) in D:\a\1\s\Kerberos.NET\Entities\SpNego\ContextToken.cs:line 43
at Kerberos.NET.Entities.KerberosContextToken.DecryptApReq(KeyTable keys) in D:\a\1\s\Kerberos.NET\Entities\SpNego\KerberosContextToken.cs:line 38
at Kerberos.NET.Entities.NegotiateContextToken.DecryptApReq(KeyTable keys) in D:\a\1\s\Kerberos.NET\Entities\SpNego\NegotiateContextToken.cs:line 38
at Kerberos.NET.KerberosValidator.Validate(ReadOnlyMemory`1 requestBytes) in D:\a\1\s\Kerberos.NET\KerberosValidator.cs:line 70
at Kerberos.NET.KerberosAuthenticator.Authenticate(ReadOnlyMemory`1 token) in D:\a\1\s\Kerberos.NET\KerberosAuthenticator.cs:line 73
at Kerberos.NET.KerberosAuthenticator.Authenticate(Byte[] token) in D:\a\1\s\Kerberos.NET\KerberosAuthenticator.cs:line 69
at Kerberos.NET.KerberosAuthenticator.Authenticate(String token) in D:\a\1\s\Kerberos.NET\KerberosAuthenticator.cs:line 65
Splitting the KeyTable
into its entries, creating dedicated validators (one for each key) and selecting the right one for a given ticket (with some ugly workaround code to determine the ticket's SPN) works as expected and the validator for the right key also accepts the provided ticket.
To Reproduce
- Create multiple service users with multiple SPNs (for multiple hostnames) for a single service in a Windows AD domain
- Export the secrets for these SPNs into dedicated keytab files using
ktpass
- Merge these keytab files using the MIT Kerberos
ktutil
binary - Use the resulting keytab file to create a Kerberos.NET KerberosValidator
- Access the service with a Kerberos ticket for an SPN that is not the first entry in said keytab file
- The validator will throw the mentioned exception
Expected behavior
The validator should pick the right key for the specified SPN and succeed in validating a valid ticket.
Additional context
I dug into the matching logic that decides whether a KeyTable
entry corresponds to the SPN of a given ticket and found that the approach of KrbPrincipalName.Matches() doesn't work properly for Windows AD keytab entries and Windows AD tickets:
The method compares the output of KrbPrincipalName.MakeFullName() applied to both principal names being compared. MakeFullName
replaces the "service" portion of the principal name (as dubbed by RFC4120, i.e. the protocol such as HTTP/
) with the string host/
ONLY IF the principal name was split into two or more segments already. Upon inspecting the keytab entries and tickets issued by Windows AD servers, I noticed that the tickets specified a Name
that has been split into its components (i.e. HTTP
and the myhost.mydomain.com
) whereas the keytab entry exported by Windows' ktpass
utility contains the Name
as a single, concatenated string (i.e. HTTP/myhost.mydomain.com
).
This means that the Matches()
call will end up comparing "host/myhost.mydomain.com"
to "HTTP/myhost.mydomain.com"
and deducing that the keytab entry and ticket aren't compatible. I'm adding two screenshots from bruce
's kdecode
utility to illustrate the issue in the keytab/ticket structure.
Screenshots
Note that this depicts the decoded keytab file as exported by Windows' ktpass
utility. This keytab entry hasn't been passed into ktutil
yet, so I'm quite certain that the MIT Kerberos implementation is not to blame.