The Kerberos.NET library relies on a collection of cryptographic primitives to implement the protocol. This post describes what those primitives are and how they're used.
Kerberos as used in the real world is an implementation of a handful of specifications. If you take a look at the deep dive document you can see the various specs that make up the functional aspects of the protocol. There are additional specs that define the cryptographic algorithms used by Kerberos:
You can follow those specs further down the rabbit hole into distinct primitives. These specs in particular define the characteristics of things like key derivation, padding, negotiation, etc.
This library implements the RFC's above and, with one exception, relies on the platform to do any real crypto operations. All of these primitives are exposed via a platform abstraction layer, the CryptoPal. This Pal exposes the dozen or so operations required by the library and in most cases just wraps .NET's APIs.
There is one primitive implemented directly in the library: RC4. It's only there for backwards compatibility and should not be used.
The publicly consumable API's are the Kerberos-specific algorithms. They directly implement the above RFCs, including things like key-stretching and derivation. The logical structure looks something like this:
The Kerberos.NET library has undergone significant redevelopment over the last year and has introduced many new features across both client and server. This post dives deep into how this project is designed and intended to be used.
There's no guarantee the implementations of these additional protocols is strictly compliant, but they are implemented enough to be compatible with most major vendors.
The library itself is built against .NET Standard 2.0 to keep it compatible with .NET Framework. It has a Nuget package:
PM> Install-Package Kerberos.NET
There are three logical components of the Kerberos.NET library: client, KDC, and application server. Each component relies on shared services for things like cryptographic primitives or message encoding. The logical structure looks a little like this:
The Kerberos Client
The client is implemented as KerberosClient and can be extended through overriding methods and properties in derived types. The client is transport agnostic, meaning it relies on other classes to provide on-the-wire support.
There are three built-in transport types: TCP, UDP, and HTTPS. The TCP transport is the only one enabled by default because it's the most reliable in real networks. The HTTPS transport is actually an implementation of the [MS-KKDCP] proxy protocol, which is effectively just the Kerberos TCP messages wrapped in HTTP over TLS.
The client is intended to be scoped per-user and hides most of the protocol goo from the caller for the sake of simplicity. The fundamental flow is
Get credentials for user (username+password or X509 Certificate)
Call client.Authenticate(creds) to request a TGT (ticket-granting-ticket)
Call client.GetServiceTicket(spn) to get a ticket to a specific service
The tickets are cached in the client instance but do not persist across instantiations (an exercise left to the caller).
Kerberos messages are generated by client and all the cryptographic manipulations for encryption and signing are handled by the KerberosCredential derived classes. There are three native credential types supported by this library (so far): KerberosPasswordCredential, KeytabCredential, and KerberosAsymmetricCredential. These credentials implement their named pre-authentication and key exchange flows.
Pa-Enc-Timestamp is the default pre-auth method and is implemented in the KerberosCredential base class.
Password credentials are used for providing the long-term encryption key for the Pa-Enc-Timestamp pre-auth data and AS-REP session key. Passwords require key derivation with salts as provided by the KDC in Krb-Error messages in order to reliably handle AES ciphers.
Keytab credentials are optimizations on password credentials and do not require key derivation. This could be problematic for callers if the KDC decides to rekey and is only recommended in controlled environments.
Asymmetric credentials are RSA or ECC keys for use with PKINIT. They're often protected in hardware through smart cards. The PKINIT client auth flow only implements the Diffie-Hellman key exchange for forward-secrecy of session keys. DH keys are not reused, but callers can implement a caching mechanism by overriding the CacheKeyAgreementParameters and StartKeyAgreement methods. ECDH is not supported as of today (Dec 2019), but it's in the works.
The Key Distribution Center (KDC)
The KDC is a server implementation that authenticates users and issues tickets. The KDC implements AS-REQ and TGS-REQ message flows through message handlers, which receive messages through a protocol listener. The only built-in protocol listener is for TCP sockets. Protocol listeners are left to the caller to implement as it's most likely going to need to be customized for their use case. A sample KDC server can be located in the KerberosKdcHostApp project.
AS-REQ and TGS-REQ Message Handling
Messages are received by a KdcServer instance, which has message handlers registered against it. Messages are parsed by the instance by peeking at the first ASN.1 tag it sees. Each tag type is handled by dedicated handlers. There are two handlers built-in: KdcAsReqMessageHandler and KdcTgsReqMessageHandler. It's up to the message handler to validate for protocol correctness and respond as necessary.
Pre-authentication is handled through a similar mechanism aptly named pre-auth handlers. A pre-auth handler is invoked after the first bit of the AS-REQ message handling has parsed the requested username and has looked up the user through the IRealmService. Pre-authentication requirements are determined based on properties of the found user object. If a user is found to require pre-auth and the requisite pre-auth data isn't present, an error is returned to the client.
User and Service Principals
This library doesn't have a strong opinion on what principals or services should look like and only requires a small amount of information to fulfill most requests. The logical flow of a full Kerberos authentication looks like this as far as principals are concerned:
AS-REQ is received and the Body.CName is parsed
The CName is passed to an IRealmService => IPrincipalService.Find()
The result is IKerberosPrincipal which indicates what pre-auth types are enabled or required, what long term secrets it supports, and how to generate the user PAC
The well-known service krbtgt is located through IRealmService => IPrincipalService.Find()
A TGT is generated for the user and encrypted to the krbtgt service key and wrapped in an AS-REP message
The AS-REP is returned to the caller
TGS-REQ is received and the TGT is extracted from the message
The TGT is decrypted by looking up the krbtgt service account key through IRealmService => IPrincipalService.Find()
The TGS-REQ target service is located through IRealmService => IPrincipalService.Find()
The user principal is validated against whether they can get a ticket to the service (by default always yes)
A new ticket for the user is created and encrypted against the service key located in step 9.
The IRealmService is a simple service provider to easily expose principal queries and access realm settings.
The Application Server
The third component of this library is actually the original driver for building this library. Ticket validation is handled by an application service that has received a Kerberos ticket from a client such as web browser and wants to turn it into a ClaimsIdentity.
There are two parts two validation: decrypting the ticket and converting it into an identity.
Decrypting the ticket is handled through by KerberosValidator which decrypts the message, checks timestamps, and verifies it's not a replay.
Conversion to ClaimsIdentity is handled by KerberosAuthenticator which extracts principal data as well as PAC authorization data making up group memberships and claims.
The Privilege Attribute Certificate
The PAC is where authorization data like group memberships and claims are carried in Kerberos tickets. This is a Windows-specific detail and as such has some unique characteristics that don't match the rest of the Kerberos protocol.
The PAC is a sequentially ordered C-like structure containing ten or so fields of data. The primary structure is KERB_VALIDATION_INFO, which is an NDR-encoded message (also known as RPC encoding). The NDR marshaling is handled by the NdrBuffer class and relies on each encoded struct to provide their own marshal/unmarshal steps.
The ASN.1 Serializer
Kerberos is a protocol serialized using ASN.1 DER encoding. There aren't many good serializers out in the wild and those that are good aren't particularly developer-friendly. The .NET team has an ASN.1 library built into .NET, so I made a fork of that with some minors tweaks. The gist of these tweaks were:
Set default precisions that match Kerberos specs
Added GeneralString which Kerberos treats as IA5Encoding for historical reasons
Modified the generator to output classes instead of structs (probably the most heinous tweak)
Added message inheritance because a bunch of the messages are semantically the same
Messages are represented as entities during build time and are run through an XSLT transform producing strongly-typed classes. These classes are public and designed to be used as first-class citizens when interacting with the library. Encoding and decoding is considered an implementation detail and marked as internal.
Tl;dr; It’s really not. Kerberos is showing its age, but it has served us well over the years. As we build new protocols we should remember all the things we got right with it, and account for all the things we got wrong.
Tl;dr; It’s really not. Kerberos is showing its age, but it has served us well over the years. As we build new protocols we should remember all the things we got right with it, and account for all the things we got wrong.
A bit of a Background on Kerberos
Kerberos is an authentication protocol. It allows a party (A) to prove to another party (B) they are who they say they are by having a third party (C) vouch for them.
In technical terms this means a principal can prove to another principal who they are by authenticating themselves to an authentication server.
In practical terms a user can log in to their application by authenticating to Active Directory using a password.
This protocol is built on a set of message exchanges.
The following is mostly accurate, but a few liberties have been taken for the sake of simplification. All users are principals, but not all principals are users. All principals can authenticate to all principals, but the type of principal dictates properties of messages.
Here we’re looking at a standard user principal authenticating to a service principal for the simplest explanation as it’s the most common.
Authenticating to the Authentication Server
The first message exchange is between the (user) principal and the authentication server.
The user creates a message, encrypts it using their password, and sends it to the authentication server. The server has knowledge of the user’s password and can decrypt the message, proving to each party they are who they say they are respectively (within the limit of how easy it is to guess or steal this password). The server then generates a new message containing a ticket and a session key, encrypted against the user password, returning it to the user.
The user can decrypt the message and now has a ticket and session key. The session key can be used to secure any future communications between the two parties, and the ticket is used as evidence of authentication for future requests requiring authentication. This ticket is called the ticket granting ticket (TGT) — you use it to get other tickets to services.
The TGT contains identifying information about the user as well as a copy of the negotiated session key, and is encrypted to the password of a special account named krbtgt. Only the authentication server knows the password for krbtgt.
Authenticating to the Service
The second message exchange is between the (user) principal and (service) principal. It’s a bit more complicated than the first exchange.
A protocol higher in the stack indicates to the user that the service requires authentication. Kerberos doesn’t much care; it’s up to the user to initiate the exchange. The user determines the service principal name (SPN) of this service and generates a new message to the ticket granting server asking for a service ticket.
This message from the user to the ticket granting server contains the ticket-granting-ticket (TGT) from the first exchange. The message is encrypted to the previously negotiated session key. The ticket granting server also has knowledge of the krbtgt password and so can decrypt the TGT and extract the session key.
The ticket granting server can then validate the message and generate a response containing a service ticket for the service principal containing identifying information from the TGT The service ticket is encrypted against the password of the service principal. Only the ticket granting server and service principal know this password. The encrypted ticket is wrapped in a message that is then encrypted using the previously negotiated session key and returned to the user.
The user is able to decrypt the message using the session key and can forward the service ticket to the service. The user can’t see the contents of the ticket (and therefore can’t manipulate it). All the user can do is forward it. The service receives the encrypted service ticket and decrypts it using its own password. The service now knows the identification of the user. The service can optionally respond with a final message encrypted with the subsession key to prove receipt or require a seperate subsession key.
Protected Application Layer Exchanges
Now that the service has verified the user and the user has verified the service it’s possible for them to use a key derived from the subsession key to safely continue communications between the user and the service.
The Pro’s and Con’s of Kerberos
It shouldn’t come as a surprise that Kerberos has many positive and negative traits.
Kerberos is Cryptographically Sound
This turns out to be a useful property of an authentication protocol. You can generally prove the cryptographic promises of each leg are secure without making too many assumptions.
A principal’s identity is proved by the secret properties of the password and hardness of the encrypting algorithm; the identity of the server is proven conversely by knowing the password and being able to decrypt the message without having to reveal the password to any party. This is an often overlooked property of most authentication protocols.
The multileg exchange is provably secure by guaranteeing only the final recipients can decrypt messages; messages can’t be modified because they’re signed by the various authoritative sources; and messages can’t be replayed because they have counters.
All of these properties accrue to a confidential, tamper-evident, authenticated, non-repudiated authentication protocol. Most other protocols don’t have all these properties and rely on external protocols to provide these guarantees.
Cryptographic Properties Extend to Services
Services that rely on the derived session keys apply the properties of the authenticated exchanges to the derived key. This is non-trivial to get right in the best of cases so having it available to you and bound to your authentication service is useful.
The Kerberos V5 RFC is mostly complete. It provides an end-to-end solution for multi-party authentication with solutions for transport, retry semantics, supported credential types, etc. This is useful because it limits one’s ability to break interoperability while still being true to the specification. A specification is a great equalizer. You either built it correctly or you didn’t.
Implementors will invariably break interop one way or another, but the break will be contravened by the spec. There is utility in being able to go to the implementor, point to the gap, and ask them to fix it.
There are of course areas where there are gaps; no spec is perfect. However, there are at least 12 recognized specs intended to ratify these gaps and introduce improvements.
It has Provisions for Authenticating Both Parties
Most multi-party federation protocols only dictate how you authenticate the final leg of the trip, meaning from the identity provider (authentication service) to the relying party (service). This means you’re left on your own to figure out how to authenticate to the identity provider. This is not a bad thing, but it starts placing assumptions on the properties of the authenticated identity. This leads to questionable interop.
Kerberos is pretty clear about how a user authenticates to the authentication service to get tickets. The implementor doesn’t need to design their own system.
Windows Integrated Authentication relies heavily on Kerberos; this means means any application running on Windows can authenticate users or services using Kerberos. Most other major operating systems have an ability to do Kerberos, either natively, or through third party implementations like Heimdal or MIT.
Applications can opt in to Kerberos authentication using SSPI or GSS API’s with relatively little or no effort. High level development frameworks often wrap these API’s further to simplify usage.
Most organizations have Active Directory, which is amongst other things an implementation of the Kerberos AS and TGS.
This generally means you wont be seriously limiting yourself if all you relied on for authentication was Kerberos.
Kerberos Credential Protection is Well Understood
There is a fairly large gap in the second exchange, where if an attacker can steal the TGT, they can take that TGT and use it on other clients. This means they can impersonate the user as long as the TGT is valid, and all the services involved really won’t know any better.
This is an unfortunate side effect most authentication systems have because you’re exchanging an expensive authentication process (user entering password) with an inexpensive process (caching a TGT locally) to optimize for performance and user annoyance.
There are ways to solve this problem, which is generally described as proof-of-posession, where you stamp the ticket with a proof of work. The entity receiving the ticket can request you prove ownership by doing work that only you can do and is incredibly difficult for an attacker to forge. The simplest form is by signing a challenge with a secret only the user knows, such as with a private key stored on a smart card or FIDO2 device.
But down the rabbit hole we go because now the verifier needs to know the public key ahead of time, and… We’ve increased complexity of the system making it more fragile and difficult to use correctly.
It’s like entering a carnival. You can either pay each ride independently, which is annoying, time consuming, and potentially dangerous for all parties, or you can go to the ticket booth, buy a bunch of tickets, and use those tickets as evidence you paid money. It’s easier and safer, and it’s not the end of the world if an attacker steals your tickets because they’re only useful for a short period of time.
Given that this is a well known issue, we’ve done a lot of work making sure you can’t steal these tickets. In Windows we have things like centralized protection of secrets in the LSA, which moves any dangerous secrets out of a given application (making it hard to steal secrets) as well as Credential Guard, which uses virtualization-based security to move all the important secrets to a seperate virtual machine so compromising LSA won’t get you anything (making it extremely difficult to steal secrets).
Conversely other authentication protocols are less well understood, primarily because they haven’t been around as long and aren’t as widely adopted.
Cross-Realm Trusts Let you Create Security Boundaries
Kerberos has a concept of cross-realm authentication. This allows two organizations to trust the identities issued from the either organization. They can be directional or bidirectional meaning organization A might trust identities from organization B, but B won’t trust A.
This has untold utility, because you can isolate resources based on the amount of security required by the resource. There aren’t many authentication protocols that have a built-in capability for this without presenting raw credentials.
It’s easy to find fault in things so this is primarily about the big issues people have complained about in the past. We’re not talking minor nits.
The Complexity might Kill You
There’s an old joke: I went to explain Kerberos to someone and we both walked away not understanding it.
Exhibit A: See the first section.
Joking aside, this is a complicated protocol to understand completely. It doesn’t matter to end users so much, but it seriously degrades ones ability to troubleshoot failures, verify security guarantees, or extend in any meaningful way.
Cross-Realm Trusts are Complicated
Cross-realm trusts can be transitive, meaning if A trusts B and B trusts C, A could trust C. In principle this is simple and has a logic to it, but in practice it’s difficult to fit in your head and reason about.
It’s also difficult to reason about authorization to resources. Presenting an identity from another realm and trusting it hasn’t been tampered with is one thing, but deciding that the claims presented about the user should be considered during authorization decisions is something completely different. You are often forced to explicitly set rules within the resource realm instead of implicitly trusting the information because the authorization rules are contextually relevant.
(Unconstrained) Delegated Authentication needs to Die
Once a service has a users identity, it’s useful to forward that identity to another service as proof that the first service is operating on behalf of the user. This means you don’t end up with applications having the highest possible permissions any user will need and instead just rely on the user having the necessary permission. Compromising downstream resources via the application requires having an active user versus having unfettered access.
Kerberos got delegation wrong though. It lets the application operate as the user and access further resources on behalf of that user, but it’s not constrained to specific services. An application might only need access to a single database server, but when given delegation rights, it can access any resource as that user.
However, we have a solution to this which is constrained delegation and resource-based constrained delegation, which specifically lists which principals can be delegated to, as well as what services can receive those delegated tickets.
The problem is that most people don’t understand how constrained delegation works which tends to lead to falling back to unconstrained delegation.
Cryptographic Primitives are Starting to Smell
DES was the standard when Kerberos was first published. It’s still supported by all the major implementations in one form or another and most enterprises still have it enabled.
RC4 is still kicking and is often the default especially when trying to work across multiple vendors.
Key derivation at its lowest form is still generally based on MD4.
Integrity is based on a truncated SHA1 HMAC in most implementations. SHA256 has recently been standardized, but see the next few issues.
Ossification makes Changing Cryptographic Primitives Impossible
We learned this with TLS 1.3: protocols don’t like change. Things start to harden and become brittle when implementations make assumptions about protocol designs. Every implementation makes assumptions in one way or another and often they become difficult to fix.
Fundamental overhauls break everything even if the original protocols were designed to support future changes.
Changes need Critical Mass before they can be Turned on by Default
Turning on a new cryptographic primitive like SHA256 probably won’t break much, but it can’t be default until everyone supports it. It will take a decade before most implementations support it, and who knows how long it’ll be before it can be turned on by default, because there’s always some box that won’t support it.
ASN.1 Data Formatting Suuuuuuuuuuucks
This is a personal gripe. I dislike ASN.1 because it’s a complex serialization format. It’s difficult to parse and more difficult to generate.
This is a problem because it limits one’s abilities to build Kerberos implementations and results in only a handful of libraries that are feature rich. This has the unfortunate side effect that fewer people understand the protocol (coupled with complexity) and fewer still contribute to its long term development.
Kerberos Message Structures have a Range of Serialization Formats
Another gripe of mine. There are multiple serialization formats in use depending on the type of data structure you’re looking at. Kerberos itself uses ASN.1, but MS-KILE (Microsoft’s specification of deltas) uses RPC (NDR) serialization for authorization structures. This is mostly just annoying having to flip between formats, but complicates interop.
All the Crypto is Symmetric
Kerberos is a protocol that was built back when asymmetric cryptography was too expensive to use routinely or securely. It relies entirely on shared secrets between all parties, which means anyone that’s verifying a message can spoof a message. This can be negated by asymmetric signing.
PKINIT is an extension to Kerberos that allows users to execute the first message exchange using asymmetric cryptography, but all future exchanges still rely on symmetric secrets.
We actually built a variation of Kerberos that uses asymmetric crypto called PKU2U, but it’s designed for user-to-user authentication — i.e. without a trusted third party. It was originally built for HomeGroup and is now used for point-to-point authentication using Active Directory credentials.
Initial Authentication Generally Assumes Passwords
The last point suggested PKINIT can be used for authentication, and it can, but this assumes the application knows how it works and supports certificates. It turns out this isn’t as common as you’d think, and applications tend to just prompt for passwords. There will always be legacy applications that just work that will never be updated.
The other problem here is that you’re still limited to passwords or RSA-based asymmetric keys. We’re trending towards ECC asymmetric crypto with FIDO and it’s impossible to fit that into an exchange that is universally (or at least critically) supported let alone other arbitrary authentication messages.
It should be pointed out that using passwords does have its benefits. They are incredibly simple to use when machine generated and managed.
Line of Sight to Authentication Servers is Mandatory
Line of sight to the authenticating server is often a requirement in authentication protocols, but Kerberos services tend to live only within private networks. Whether you should move them to the open internet is up for debate (actually it’s not — don’t do it), but most organizations choose not to do it and would rather rely on a service born on the internet if needed.
So What’s the Takeway?
There are certainly a lot of problems with Kerberos — they are the criticisms from heavy adoption. The protocol still has value, but it’s starting to show its age and we need to look forward to what replaces it. As we move away from it we need to remember that Kerberos got quite a few things right and learn from that.