There’s a common problem that many applications run in to when executing cryptographic operations, and that’s the fact that the keys they use tend to exist within the application itself. This is problematic because there’s no protection of the keys — the keys are recoverable if you get a dump of the application memory, or you’re able to execute arbitrary code within the application. The solution to this problem is relatively straightforward — keep the keys out of the application.

In order for that to be effective you need to also move the crypto operations out of the application too. This means any attacks on the application won’t yield the keys. This is the fundamental design for services like Azure Key Vault, Amazon CloudHSM, or any other HSM service or device. In fact, even Windows subscribes to this design with CNG keys using LSASS.

I was thinking about this problem over the weekend and realized there isn’t really any good reference architecture out there that shows you how to build this design into your application. The services I mentioned earlier do great jobs at protecting secrets, but they’re kind of designed with certain applications in mind — greenfield, cloud first, or disconnected. This can make it difficult to migrate existing applications to use these services, or maybe you just can’t use them for whatever reason (regulatory, legal, etc.). On top of all that, you don’t even know how to start.

So, I did something dumb. I built a reference implementation that lets you move crypto operations out of your application and into a separate process.

Introducing: Enclave.NET.

READ THIS SECURITY WARNING

This is a reference implementation. That means it’s not designed to handle production loads, and absolutely is not built to withstand attack. It’s a sample intended to show how you might offload certain operations. There are probably some horrible bugs in here and there might even be vulnerabilities.

Was that sufficiently scary enough?

The basic idea is simple. You have an application that hosts a service. The service has a set of commands available to it:

  • Generate key
  • Encrypt
  • Decrypt
  • Sign
  • Validate

Your application calls these commands with the necessary payloads, the service does the thing, and returns a result. The service is HTTP-based, and protected with pinned client certificates.

The crypto operations are real, backed by the jose-jwt library, but they’re ephemeral — the keys are just stored in memory. The idea is that you can inject your own implementations as you see fit, so any migrations you might undergo can be gradual and painless.

There are two classes that need to be implemented — the crypto operations class, and the storage service. You can use the built-in crypto operations class InMemoryCryptoProcessor at your own risk, but you absolutely need to implement the storage, lest you lose all the keys when the app shuts down.

Crypto Processor

Storage Service

You can modify the startup code on your own, or you can implement IStartupTransform and configure it:

Calling the service is simple:

For more information take a look at the sample app.

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.

In my last post I talked about how Azure AD does Kerberos Single Sign-On. Conceptually it’s a simple process, but when you dig into the details of the implementation, there are some serious hurdles to overcome.

The Active Directory side of things is straightforward — it’s just a matter of manually creating an SPN and keeping the secret in sync. It gets really complicated on the Azure AD side of things though.

Consider the history of web-based Kerberos. IIS has supported this for decades by way of an ISAPI HTTP module that parses out the header and hands it off to the Negotiate SSP, which eventually gets it into the Kerberos SSP within LSASS to validate. This is a useful design because it moves any secret keys out of the IIS worker process and into the protected LSASS process. The problem with this is that you don’t have a whole lot of control over the validation process.

There are a number of knobs you can fiddle with in the process like specifying custom SPN credentials using AcquireCredentialHandle as well as what sort of flags you want to use, but as far as I can tell that’s about the extent of what you can modify because the SSP makes some specific demands that the ticket comes from a trusted authority, which in this case is the domain which the machine is joined. This seriously limits our abilities to use the built in Windows functions in the cloud.

One interesting behavior worth noting though is that you can sort of see that a ticket is validated even if its not from a trusted authority because the audit entries show up differently. It logs it as a decrypted ticket, but that it’s from an unknown authority and therefore cannot be accepted. This leads me to believe you might be able to get in the middle of it and validate it yourself, but I haven’t been able to figure it out despite my best efforts.

Given these limitations I decided to see what I can do about creating a library that lets you authenticate Kerberos tickets manually. Now, there are a number of available libraries out there, but they’re either written in Java (eww) or C/C++ (oww), and they’re difficult to understand. Enter Kerberos.NET, a managed ticket parser and validator.

It’s pretty easy to use. Create an instance of the validator, pass in the security key, and validate a ticket:

Here’s the cool thing: it’s written entirely in managed code and doesn’t take any hard dependencies on native components. That means it’s reasonably portable and can run any environment where ASP.NET Core can run.

The caveat of course is that I haven’t tried it yet.

Now, some of you might be wondering why on earth you want to use something like this over a web-friendly federation protocol like SAML or OpenID Connect. Well, frankly, you shouldn’t use it (gasp!) — federated protocols are a better solution these days. But if you need real Kerberos out in the cloud you can use …a vetted option like the SPNEGO module for Nginx (I don’t actually know if it’s vetted, but you get my point). In other words don’t use this library unless someone much smarter than me has vetted it (and there are plenty of people that fit that requirement).

Additionally, there are some important things to keep in mind if you do decide to use this module.

  • It currently only supports RC4 (I know, I know, but AES CTS is painful in .NET) Update: This is fixed!
  • The validation mechanism hasn’t been vetted
  • The token cache is not self-managing (it’ll leak memory like a sieve)
  • It assumes crypto-validation is good enough — it doesn’t check that SPNs match
  • It doesn’t parse the PAC data structure and you therefore don’t get access to any of that useful data Update: This is also fixed!

There are more reasons not to use this library too I’m sure.

Enjoy!

For those wondering about the picture of the puppy… Kerberos (AKA cerberus) is the name of the three-headed dog guarding Hades’ underworld. It probably stands for “spotted”, meaning Hades named his dog Spot.