Kerberos

Print Friendly, PDF & Email

Kerberos is an authentication protocol originally developed by MIT and implemented by various vendors and systems; probably its best description is in the first paragraph that can be found in the MIT Kerberos site:

Kerberos is a network authentication protocol. It is designed to provide strong authentication for client/server applications by using secret-key cryptography. A free implementation of this protocol is available from the Massachusetts Institute of Technology. Kerberos is available in many commercial products as well.

In the WWW can be found hundreds of sites where the protocol is fully described but the MIT site remains the primarily place in which one can get lost getting details about Kerberos – given that version IV is a late-1980 definition, I wouldn’t say it’s a young boy.

Recently I got to support SSO registration to our proprietary platform through Kerberos from our legacy client, a Win32/.NET application written in C/C++/Managed C++/C#. At the beginning I was quite sure I would have found wrapper classes in .NET that would make the whole thing as easy as a pie… soon I had to face the cold, hard reality. For some kind of mysterious reason, Microsoft has decided that among the tons of classes that in .NET wrap native features, Kerberos doesn’t deserve a single (meaningful) class.

Briefly – why is it called “Kerberos”? Because just like the mythological three-headed dog at the entrance of the Hades, three actors are involved in the protocol: the Client, the Service Server and Authentication Server; usually client and server negotiates the protocol, setup the channel, prepares and shares the key and so on – with Kerberos the Authentication Server is the keys repository and guarantees that the whole thing can go on the right way.

In fact, it is a bit more complicated than this: for example, when we logon in an interactive session in Windows we are already kicking in the Kerberos thing – but let’s proceed in an orderly manner.

The first step is to get a Ticket Granting Ticket: this is released by the AS, together with a session key.
The second step is to get a Ticket Granting Service for the service with wich we want to interact – to do so we need the TGT.
Finally, we can use the TGS to setup the connection with the Service.

In Windows, Kerberos is implemented as part of the Security Support Provider layer, exposed through its interface (the SSPI): among the modules there is the LAN Manager or -of course- Kerberos; so, I didn’t expect to find functions such as “GetTicketGrantingTicket()” or “GetTicketGrantingService()” but at least something reasonably easy to use. It ended up that it is easier to find people that has decided to go through RFC4120 and re-implement the whole protocol than find pages that explains in a bunch of easy step how to ask to the AD a TGS to grant access to a AD-registered service. To me this seamed a totally crazy thing: Windows uses Kerberos (it’s the default authentication protocol used by Active Directory) so it’s obvious that it has an implementation of it, and since SPPI is a public interface there must be function calls I can use to leverage it.

Getting the Ticket Granting Ticket

The first thing I (wrongly) thought I had to do was to get the TGT: “well, to get a TGS I have to pass by the TGT, so I probably have to get the TGT first”.

This starts with a connection to the Local Security Authority:

HANDLE mLsaHandle;
NTSTATUS Status;

Status = LsaConnectUntrusted(mLsaHandle);

all these functions return an NTSTATUS: I won’t use it in the short excerpts I’m writing here but it makes of course sense to check it; once we have a connection handle, we can use it to obtaint the Kerberos package ID:

ULONG mPackageId;
LSA_STRING Name;
Name.Buffer = (PCHAR)MICROSOFT_KERBEROS_NAME_A;
Name.Length = (USHORT)strlen(Name.Buffer);
Name.MaximumLength = Name.Length + 1;

Status = LsaLookupAuthenticationPackage(mLsaHandle, &Name, &mPackageId);

if everything went fine, we have now the ID with which we can ask for the TGT, using a properly formatted KERB_QUERY_TKT_CACHE_REQUEST message:

bool GetTicketGrantingTicket()
{</pre>
<pre>    NTSTATUS Status = 0;</pre>
<pre>    NTSTATUS SubStatus = 0;

    KERB_QUERY_TKT_CACHE_REQUEST CacheRequest;
    PKERB_RETRIEVE_TKT_RESPONSE pTicketResponse = NULL;
    ULONG ResponseSize;

    CacheRequest.MessageType = KerbRetrieveTicketMessage;

    // Set LogonId to 0 to request the current user's logon session
    CacheRequest.LogonId.LowPart = 0;
    CacheRequest.LogonId.HighPart = 0;

    Status = LsaCallAuthenticationPackage(
        mLsaHandle,
        mPackageId,
        &CacheRequest,
        sizeof(CacheRequest),
        (PVOID*)&pTicketResponse,
        &ResponseSize,
        &SubStatus);

    // check Status/SubStatus

    // free the return buffer
    LsaFreeReturnBuffer(pTicketResponse);

    return true;
}

Great! Now that we have the TGT, getting the TGS will probably require some kind of similar sequence of steps.

Getting the Ticket Granting Service

We have understood that getting tickets is a matter of querying the LSA with proper messages (after all, Kerberos is a message based protocol, right?); to get the TGS we perform another call using the “service name” for which we want the TGS:

PKERB_EXTERNAL_TICKET GetTicketGrantingService(const char* service)
{
    NTSTATUS Status;
    PKERB_RETRIEVE_TKT_REQUEST CacheRequest = NULL;
    PKERB_RETRIEVE_TKT_RESPONSE CacheResponse = NULL;
    PKERB_EXTERNAL_TICKET Ticket = NULL;
    ULONG ResponseSize;
    NTSTATUS SubStatus;
    BOOLEAN Trusted = TRUE;
    BOOLEAN Success = FALSE;
    UNICODE_STRING Target = { 0 };
    UNICODE_STRING Target2 = { 0 };
    stringstream ss;

    wchar_t * wService = GetWC(service); // converts service in wchar
    InitUnicodeString(&Target2, wService); // properly init the UNICODE string

    CacheRequest = (PKERB_RETRIEVE_TKT_REQUEST)LocalAlloc(LMEM_ZEROINIT, Target2.Length + sizeof(KERB_RETRIEVE_TKT_REQUEST));

    CacheRequest->MessageType = KerbRetrieveEncodedTicketMessage;
    CacheRequest->LogonId.LowPart = 0;
    CacheRequest->LogonId.HighPart = 0;
    CacheRequest->EncryptionType = KERB_ETYPE_RC4_HMAC_NT;
    CacheRequest->CacheOptions = KERB_RETRIEVE_TICKET_AS_KERB_CRED;
    Target.Buffer = (LPWSTR)(CacheRequest + 1);
    Target.Length = Target2.Length;
    Target.MaximumLength = Target2.MaximumLength;
    CopyMemory(Target.Buffer, Target2.Buffer, Target2.Length);
    CacheRequest->TargetName = Target;

    delete wService;

    Status = LsaCallAuthenticationPackage(
        mLsaHandle,
        mPackageId,
        CacheRequest,
        Target2.Length + sizeof(KERB_RETRIEVE_TKT_REQUEST),
        (PVOID *)&CacheResponse,
        &ResponseSize,
        &SubStatus
    );

    // check errors

    if (CacheRequest)
    {
        LocalFree(CacheRequest);
    }

    return Ticket;
}

finally, since we are good Windows citizens, we cleanup the session:

bool TearDown()
{
    //...
    NTSTATUS res = LsaDeregisterLogonProcess(mLsaHandle);
    // as usual, check errors
    return true;
}

Great! Now we have the TGS and thus we can use it to open a session with the Service Server!

Wrong.

Security contexts

The factual thing about the previous steps is that we really got a TGT and a TGS: we can check them and we’ll recognize some properties (the domain, for example); one thing that is strange to me is that to get a TGS we didn’t have to pass by the TGT – oh well, probably in a Windows session it is implicitly the TGT I already got when I started the interactive session.

The bad thing is that in a real scenario a simple TGS is not enough to establish a session with a service: the ticket used in the session startup contains some other properties that are not part of a TGS.

To get a ticket valid to start a client/service session we have to obtain a more detailed opaque token that the service will submit to the local security implementation to verify that the client has the permission to use the service. To do so we have first (as often happens in Windows) acquire a session handle and then initialise a new security context:

// get a new credentials handle
SecHandle credHandle;
SECURITY_INTEGER pts;
SECURITY_STATUS ss = AcquireCredentialsHandle(
    NULL,
     MICROSOFT_KERBEROS_NAME_W,
    SECPKG_CRED_OUTBOUND,
    NULL,
    NULL,
    NULL,
    NULL,
    &credHandle,
    &pts);
// check errors...

then we can ask an opaque TGS:


wchar_t* spn = GetWC(servicePrincipalName);  // SPN in wide char

SecBuffer outSecBuffer;
SecBufferDesc output;

outSecBuffer.pvBuffer = NULL;
outSecBuffer.BufferType = SECBUFFER_TOKEN;
outSecBuffer.cbBuffer = 0;
output.ulVersion = SECBUFFER_VERSION;
output.cBuffers = 1;
output.pBuffers = &outSecBuffer;

CtxtHandle contextHandle;
ULONG pfContextAttr;
TimeStamp timeStamp;

SECURITY_STATUS secCtxRes = InitializeSecurityContext(
    &credHandle,
    NULL,
    spn,
    ISC_REQ_CONNECTION | ISC_REQ_ALLOCATE_MEMORY,
    0,
    SECURITY_NATIVE_DREP,
    NULL,
    0,
    &contextHandle,
    &output,
    &pfContextAttr,
    &timeStamp);

// check error

int count = 0;
while (secCtxRes == SEC_I_CONTINUE_NEEDED)
{ // this part is not tested
    secCtxRes = InitializeSecurityContext(
        &credHandle,
        &contextHandle,
        spn,
        ISC_REQ_CONNECTION | ISC_REQ_ALLOCATE_MEMORY,
        0,
        SECURITY_NATIVE_DREP,
        &output,
        0,
        &contextHandle,
        &output,
        &pfContextAttr,
        &timeStamp);
    if (count > 5) break;
}

output.pBuffers = NULL;
delete spn;

FreeContextBuffer(outSecBuffer.pvBuffer);
DeleteSecurityContext(&contextHandle);

// outSecBuffer.pvBuffer

In outSecBuffer.pvBuffer there’s finally our valid opaque ticket.

Conclusion

Mastering Kerberos is not easy; quite often, from a developer perspective, we stumble upon these topics and at a first sight we think that such an operation (to be granted to use a service) should be a straightforward operation because we know that many times we need to save time for other tasks.

Our job here is not finished: we have to wrap the Win32 implementation with a proper .NET implementation (or start with C++/CLI right from the beginning), but this should be an easy exercise.

Leave a Reply

Your email address will not be published.