I started the Kerberos.NET project with a simple intention: be able to securely parse Kerberos tickets for user authentication without requiring an Active Directory infrastructure. This had been relatively successful so far, but one major milestone that I hadn’t hit yet was making sure it worked with .NET Core. It now works with .NET Core.

Porting a Project

There is no automated way to port a project to .NET Core. This is because it’s a fundamentally different way of doing things in .NET and things are bound to break (I’m sure that’s not actually the reason). There is documentation available, but it’s somewhat high-level and not necessarily useful when you’re in the weeds tracking down weird dependency issues. Given that, you’re kinda stuck just copying over the code and doing the build-break-tweak-build-wtf dance until you have something working. This was my process.

  1. Create a new .NET Standard project — Standard dictates the API level the library is compatible with; Core dictates the runtime compatibility. I didn’t have any particular targeting requirements, so I made it as broad as possible. Now any runtime supporting .NET Standard can use this library.
  2. Copy all the code from the main project into the new project — I probably could have created the .NET Standard project in the same location, but it’s often easier to start with a blank slate and move things in.
  3.  Build!
  4. Build fails — MemoryCache/ObjectCache don’t exist in the current API set. Thankfully this is isolated to just the Ticket Replay detection, so I was able to temporarily convert it to a simple dictionary. I eventually found a library that replaces the original caching library.
  5. Build fails again — SecurityIdentifier doesn’t exist in the current API set either. Doh! I wasn’t going to hold my breath waiting for this to be moved over, so I created my own class that had the same usefulness. This also gave me the opportunity to fix some ugly code required to merge RIDs into SIDs, which added a nice little performance boost.
  6. Build succeeds!
  7. Unload/remove the original .NET 4.6 projects from the solution.
  8. Adjust test project dependencies to use the new project instead.
  9. Run!

Once I was able to get the test projects up and running I could run them through the test cases and verify everything worked correctly. Everything worked, except for the AES support. 😒

Porting a Project with a Dependency

I added support for AES tickets a while back and it was built in such a way that it lived in a separate package because it had an external dependency on BouncyCastle. I’m not a fan of core packages taking dependencies on third parties, so I built it as an add-on that you could wire-in later. This worked relatively well, until I needed to migrate to .NET Core.

It turns out there are a number of Core/Standard/PCL packages for BouncyCastle, but what’s the right one? Weeeeelll, none of them, of course!

At this point I decided to suck it up and figure out how to make SHA1 do what I want. One option was to muck with the internals of the SHA1Managed class with reflection, but that turned out to be a bad idea because the original developers went out of their way to make it difficult to get access to intermediate values (there are philosophical arguments here. I don’t fault them for it, but it’s really frustrating). I considered rewriting the class based on the reference source, but that too was problematic for the same basic reason. Eventually I ended up porting the BouncyCastle implementation because it was relatively self-contained, and already worked the way I needed.

Security note: You should never trust crypto code written by some random person you found on the internet. That said, there’s a higher chance of finding a vulnerability in other parts of the code than with the port of this algorithm, so…

This actually works out well because now all the code can go back into a single package without any dependencies whatsoever. Neat!

Porting a Nuget Package

The nuget pieces didn’t really change much, but now the manifest is defined in the project file itself, and packages are built automatically.

Simpler Package Management

Now the package is just an artifact of the build, which will be useful if/when I ever move this to an automated build process.

Active Directory has had the ability to issue claims for users and devices since Server 2012. Claims allow you to add additional values to a user’s kerberos ticket and then make access decisions based on those values at the client level. This is pretty cool because you normally can only make access decisions based on group membership, which is fairly static in nature. Claims can change based on any number of factors, but originate as attributes on the user or computer object in Active Directory. Not so coincidentally, this is exactly how claims on the web work via a federation service like ADFS.

Of course, claims aren’t enabled by default on Windows for compatibility reasons. You can enable them through Group Policy:

Computer Configuration > Policies > Administrative Templates > System > KDC > KDC support for claims, compound authentication and Kerberos armoring

Allow Claims in Group Policy

You can configure claims through the Active Directory Administrative Center Dynamic Access Control.

Active Directory Administrative Center Dynamic Access Control

You can see the Claim Types option on the left hand menu. From there you can add a claim by hitting the New > Claim Type menu.

The configuration is pretty simple. Find the attribute of the user or device you want to issue, and select whether it should be issued for users or computers or both. You can configure more advanced settings below the fold, but you only need to select the attribute for this to work.

Add New Claim Type

Once the claim type is configured you can modify attributes of a user through the Attribute Editor in either ADAC or the AD Users and Computers console.

User Attributes in ADAC

That’s all it takes to get use claims. You do have to sign out and back in before these claims will take effect though since Active Directory issues claims in the Kerberos tickets, and the tickets are only issued during sign in (or the myriad other times it does, but sign out/in is the most effective).

However, once you’ve signed out and back in you can pop open PowerShell and see the claims in your token:

[System.Security.Principal.WindowsIdentity]::getcurrent().claims | fl type, value

Type : ad://ext/department:88d4d68c39060f49
Value : Super secret division

Windows 8 and higher automatically extract the claims from the PAC in the ticket and make them available in the user token. Additionally, .NET understands claims natively and can extract them from the Windows token.

And of course now Kerberos.NET! The library will automatically parse any user or device claims and stick them in to the resultant claims produced during authentication:

Kerberos Claims

No configuration necessary. The library will do all the work. Enjoy!

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.