It’s been a few months since there’s been any public activity on the project but I’ve quietly been working on cleaning it up and there’s even been a PR from the community (thanks ZhongZhaofeng!).

Part of that clean up process has been adding support for AES 128/256 tokens. At first glance you might think it’s fairly trivial to do — just run the encrypted data through an AES transform and you’re good to go — but let me tell you: it’s not that simple.

On Securing Shared Secrets

There’s primarily one big difference between how RC4 and AES are used in Kerberos, which is that AES salts the secret (DES actually does this too, but DES is dead), whereas RC4 just uses an MD4 hash of the secret. That means you need to think about how you store secrets when using the two algorithms. With RC4 you can either store the cleartext or the hash and you can still use it anywhere. With AES you need the cleartext secret any time you reconfigure things because the salt (more on this later) may necessarily change. That means the secret isn’t portable — this is a good thing. The problem though is that the salt uses information that isn’t available at runtime, meaning there’s an extra step involved when setting up the service. This is frustrating.

On Calculating Salts

A salt is just a value that makes a secret more unique. Generally they’re just random bytes or strings you tack on to the end of a secret, but in the Kerberos case it’s a special computed value based on known attributes. This actually serves a purpose because the salt can be derived from a token without needing extra information; the token would need to carry extra information if the salt was random, which might break compatibility or just increase the token size. The Kerberos spec is a bit vague about how this is supposed to work, but it’s basically just concatenating the realm with the principal names of the service:

salt = [realm] + [principalName0]...[principalNameN]
salt = FOO.COMserver01

This actually works well because it’s simple to compute and solves the portability problem. Both the ticket issuer and the ticket receiver know enough to encrypt and decrypt the token.

Enter Active Directory.

For reasons not entirely clear, Active Directory decided to do it a little differently. At least it’s documented. The difference is not trivial and introduces a piece of information that’s not in the request, which means you need prior knowledge for this to work — the new computation requires the samAccountName of service account without any trailing dollar signs.

salt = [realm.ToUpper()] + "host" + [samAccountNameWithoutDollarSign] + "." [realm.ToLower()]
salt = FOO.comhostserver01.foo.com

Yeah, I don’t know either.

So now you have a couple options. You can either pre-compute the secret/salt combo one time and just store that somewhere, or store the two values and use them at runtime. Pre-computing the key isn’t a bad idea — it’s actually how most environments work, generating a keytab file. This means the secret doesn’t need to be known by the service, but it adds a deployment step.

I recommend just storing the computed value.

On Computing the Key

I mentioned previously that pre-computing the key means the secret isn’t known by the service. This is because the key itself is cryptographically derived from the secret and salt in an overly convoluted way:

tkey = random2key(PBKDF2(secret, salt, iterations, keyLength))
key = DK(tkey, "kerberos")

Basically concatenate the secret and salt and run it through the PBKDF2 key derivation function for a given number of iterations and output a key of a particular length needed by the AES flavor, and then run it through the Kerberos DK function with a known constant string “kerberos”. Oh, and the iterations count is configurable! That means different implementations can select different values AND IT’S NOT INCLUDED ANYWHERE IN THE REQUEST BUT THAT’S OKAY JUST USE THE DEFAULT OF 4096. Oh yeah, and random2key does nothing.

On Decrypting the Token

Decrypting the token is a relatively tame process comparatively. You take that computed key and run it through that DK function again, this time including the expected usage (KrbApReq vs KrbAsReq etc.) and various other constants a few times and then run the ciphertext through the AES transform with an empty initialization vector (16 0x0 bytes). Of course, the AES mode isn’t something normal; it uses CTS mode, which is complicated and probably insecure — so much so that .NET doesn’t implement it. Thankfully it’s relatively easy to bolt on to the AES CBC mode.

You do get the real token once you run it through the decryptor though. But then you need to verify the checksum using a convoluted HMAC SHA1 scheme.

You may notice all this exists in a separate project. This is because the HMAC scheme requires access to intermediate bits of the SHA1 transform, which aren’t available using the built in .NET algorithms. Enter BouncyCastle. 🙁

Funny story: I originally thought I needed BouncyCastle for AES CTS support, but after finding their implementation broken (or more likely my usage of their implementation) I found out how to do it using the Framework implementation. If it wasn’t for the checksum I wouldn’t need BouncyCastle — doh!

That said, if anyone know how to do this using the native algorithms please let me know or fork and fix!

The resulting value is then handed back to the core implementation and we don’t care about the AES bits anymore (well, after repeating the same process above for the Authenticator).

On Cleaning Up the Code

I mentioned originally that I did some clean up as well. This was necessary to get the AES bits added in without making it hacky and weird. Now keys are treated as special objects instead of just as bytes of data. This makes it easier to secure in the future (encrypt in memory, or offload to secure processes, etc.).

I also added in PAC decoding because you get some pretty useful information like group memberships.

There’s still plenty to do to make this production worthy, but those are relatively simple to do now that all this stuff is done.

Microsoft recently released the Azure AD Single Sign On preview feature, which is a way to support Kerberos authentication in to Azure AD. The neat thing about this is that you don’t need ADFS to have an SSO experience if you’ve already got AD infrastructure in place. It works the same way as in-domain authentication, via a Kerberos ticket granting scheme.

This is a somewhat confounding feature for anyone who has experience with Kerberos in Windows because every party needs to be domain-joined for Kerberos to work. This doesn’t seem possible in the cloud considering its a) not your box, and b) it’s over the public internet. Now, you could create one gigantic AD environment in Azure and create a million trusts to each local domain, but that doesn’t scale and managing the security of it would… suck. So how does Azure AD do it?

It’s deceptively simple, actually. Kerberos is a federated authentication protocol. An authority (Active Directory — AD) issues a ticket to a user targeting the service in use which is then handed off to the service by the user. The service validates that the ticket came from a trusted source (the authority — AD) and converts it into a usable identity for the user. The validation is actually pretty simple.

  1. Both the service and authority know a secret
  2. The authority encrypts the ticket against the secret
  3. The service decrypts the ticket using the secret
  4. The ticket is validated if decryption succeeds

It’s a little more complicated than that, but that’s the gist.

Nothing about this process inherently requires being domain-joined — the only reason (well… not the only reason) you join a machine to a domain is so this secret relationship can be kept in sync.

With this bit of knowledge in hand, it’s not that big a leap to think you can remove some of the infrastructure requirements and have AD issue a ticket to the cloud. In fact, if you’ve had experience with configuring SSO into apps running on non-Windows boxes you know it’s possible because those boxes aren’t domain joined.

So enough background… how does this work?

During installation and configuration:

  1. A Computer object is created in AD representing Azure AD called AZUREADSSOACC
  2. Two Service Principal Names (SPNs) are added to the account
    • HOST/aadg.windows.net.nsatc.net
    • HOST/autologon.microsoftazuread-sso.com
  3. A password is created for this account (the secret)
  4. The secret is sent to Azure AD
  5. Both domains of the SPNs are added as intranet zones to any machine doing SSO

Authentication works as follows:

  1. When a user hits login.microsoftonline.com and enters their username, an iframe opens up and hits the autologon domain
  2. The autologon domain returns a 401 with WWW-Authenticate header, which tells the browser it should try and authenticate
  3. The browser complies since the domain is marked as being in the intranet zone
  4. The browser requests a Kerberos ticket from AD using the current user’s identity
  5. AD looks up the requesting autologon domain and finds it’s an SPN for the AZUREADSSOACC computer
  6. AD creates a ticket for the user and targets it for the AZUREADSSOACC by encrypting it using the account’s secret
  7. The browser sends the ticket to the autologon domain
  8. The autologon domain validates the ticket by decrypting it using the secret that was synced
  9. If the ticket can be decrypted and it meets the usual validation requires (time, replay, realm, etc.) then a session key is pushed up to the parent window
  10. The parent window observes the session key and auto logs in

The only sort of synchronization that occurs is that initial object creation and secret generation. AD doesn’t actually care that the service isn’t domain joined.

Technically as far a AD knows the service is domain joined by virtue of it having a computer account and secret, but that’s just semantics. AD can’t actually manage the service.

That’s all there is to it.